@hostwebhook/node-types 1.57.0 → 1.59.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.
@@ -18,7 +18,7 @@ exports.isNodeType = isNodeType;
18
18
  const PROCESSING_OUTPUTS = [
19
19
  'webhook', 'router', 'filter', 'transform', 'cache', 'code', 'rateLimiter',
20
20
  'aggregator', 'conditional', 'delay', 'schemaValidator', 'split', 'markdown', 'fileTransform', 'limit',
21
- 'emailAction', 'gmailAction', 'httpAction', 'mongoAction', 'postgresAction', 'notificationAction', 'sheetsAction', 'calendarAction', 'docsAction', 'driveAction', 'firecrawlAction', 'telegramAction', 'whatsappAction', 'discordAction', 'slackAction', 'googleContactsAction', 'googleAnalyticsAction', 'vectorStore', 'rssAction', 'socialMediaAction', 'loop',
21
+ 'emailAction', 'gmailAction', 'httpAction', 'mongoAction', 'postgresAction', 'notificationAction', 'sheetsAction', 'calendarAction', 'docsAction', 'driveAction', 'firecrawlAction', 'telegramAction', 'whatsappAction', 'discordAction', 'slackAction', 'googleContactsAction', 'googleAnalyticsAction', 'notionAction', 'vectorStore', 'rssAction', 'socialMediaAction', 'mailchimpAction', 'loop',
22
22
  ];
23
23
  /** Standard processing input sources */
24
24
  const STANDARD_INPUTS = [
@@ -26,7 +26,7 @@ const STANDARD_INPUTS = [
26
26
  'cache', 'code', 'rateLimiter', 'aggregator', 'conditional', 'delay', 'schemaValidator', 'split', 'loop', 'markdown', 'fileTransform', 'limit',
27
27
  ];
28
28
  /** Action nodes that can chain (non-terminal) */
29
- const CHAINABLE_ACTIONS = ['httpAction', 'mongoAction', 'postgresAction', 'sheetsAction', 'calendarAction', 'docsAction', 'driveAction', 'firecrawlAction', 'telegramAction', 'whatsappAction', 'discordAction', 'slackAction', 'googleContactsAction', 'googleAnalyticsAction', 'vectorStore', 'rssAction', 'socialMediaAction'];
29
+ const CHAINABLE_ACTIONS = ['httpAction', 'mongoAction', 'postgresAction', 'sheetsAction', 'calendarAction', 'docsAction', 'driveAction', 'firecrawlAction', 'telegramAction', 'whatsappAction', 'discordAction', 'slackAction', 'googleContactsAction', 'googleAnalyticsAction', 'notionAction', 'vectorStore', 'rssAction', 'socialMediaAction', 'mailchimpAction'];
30
30
  /** Full input list for action nodes (standard + chainable actions) */
31
31
  const ACTION_INPUTS = [...STANDARD_INPUTS, ...CHAINABLE_ACTIONS];
32
32
  exports.NODE_CONNECTIONS = {
@@ -77,8 +77,10 @@ exports.NODE_CONNECTIONS = {
77
77
  whatsappAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
78
78
  discordAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
79
79
  slackAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
80
+ mailchimpAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
80
81
  googleContactsAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
81
82
  googleAnalyticsAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
83
+ notionAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
82
84
  vectorStore: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
83
85
  rssAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
84
86
  socialMediaAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
@@ -48,6 +48,18 @@ exports.CREDENTIAL_TYPES = [
48
48
  { type: 'threads_oauth2' },
49
49
  { type: 'instagram_oauth2' },
50
50
  { type: 'facebook_oauth2' },
51
+ // El secreto de una integración INTERNA de Notion (empieza por `ntn_`),
52
+ // pegado por el usuario. Un solo workspace, no caduca.
53
+ //
54
+ // Va aparte de un futuro `notion_oauth` por la misma razón que
55
+ // `discord_bot` va aparte de `discord_oauth`: lo que guardan es distinto.
56
+ // Aquí hay un secreto del usuario; en OAuth habría un access_token y un
57
+ // refresh_token de una app de HostWebhook.
58
+ //
59
+ // Y el orden importa: la URL de autorización de una integración PÚBLICA de
60
+ // Notion sólo se rellena tras mandarla a revisión de Notion, así que el
61
+ // camino interno es el único que funciona el primer día.
62
+ { type: 'notion_internal' },
51
63
  ];
52
64
  /**
53
65
  * Plain string array — what the Mongoose `enum` field and the
package/dist/dispatch.js CHANGED
@@ -52,9 +52,11 @@ exports.NODE_DISPATCH = {
52
52
  telegramAction: { service: 'telegramActionsService', collection: 'telegramactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
53
53
  whatsappAction: { service: 'whatsappActionsService', collection: 'whatsappactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
54
54
  discordAction: { service: 'discordActionsService', collection: 'discordactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
55
+ mailchimpAction: { service: 'mailchimpActionsService', collection: 'mailchimpactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
55
56
  slackAction: { service: 'slackActionsService', collection: 'slackactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
56
57
  googleContactsAction: { service: 'googleContactsActionsService', collection: 'googlecontactsactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
57
58
  googleAnalyticsAction: { service: 'googleAnalyticsActionsService', collection: 'googleanalyticsactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
59
+ notionAction: { service: 'notionActionsService', collection: 'notionactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
58
60
  rssAction: { service: 'rssActionsService', collection: 'rssactions', hasFilters: false, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
59
61
  socialMediaAction: { service: 'socialMediaActionsService', collection: 'socialmediaactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
60
62
  // ── Sources — start the flow themselves, not dispatched ──
package/dist/index.d.ts CHANGED
@@ -24,6 +24,8 @@ export { WHATSAPP_OPERATIONS, isWhatsAppOperation, } from './whatsapp-operations
24
24
  export type { WhatsAppOperation } from './whatsapp-operations';
25
25
  export { DISCORD_OPERATIONS, DISCORD_OPERATION_SPECS, isDiscordOperation, } from './discord-operations';
26
26
  export type { DiscordOperation, DiscordParamSpec, DiscordOperationSpec, } from './discord-operations';
27
+ export { MAILCHIMP_OPERATIONS, MAILCHIMP_OPERATION_SPECS, MAILCHIMP_CONTACT_STATUSES, isMailchimpOperation, } from './mailchimp-operations';
28
+ export type { MailchimpOperation, MailchimpContactStatus, MailchimpParamSpec, MailchimpOperationSpec, } from './mailchimp-operations';
27
29
  export { SLACK_OPERATIONS, SLACK_OPERATION_SPECS, isSlackOperation, } from './slack-operations';
28
30
  export type { SlackOperation, SlackParamSpec, SlackOperationSpec, } from './slack-operations';
29
31
  export { SHEETS_OPERATIONS, SHEETS_OPERATION_SPECS, isSheetsOperation, } from './sheets-operations';
@@ -34,6 +36,8 @@ export { GOOGLE_CONTACTS_OPERATIONS, GOOGLE_CONTACTS_OPERATIONS_V1, GOOGLE_CONTA
34
36
  export type { GoogleContactsOperation, GoogleContactsParamSpec, GoogleContactsOperationSpec, GoogleContactsOperationGroup, } from './google-contacts-operations';
35
37
  export { GOOGLE_ANALYTICS_OPERATIONS, GOOGLE_ANALYTICS_OPERATION_SPECS, GOOGLE_ANALYTICS_DROPDOWN_OPERATIONS, isGoogleAnalyticsOperation, } from './google-analytics-operations';
36
38
  export type { GoogleAnalyticsOperation, GoogleAnalyticsParamType, GoogleAnalyticsParamSpec, GoogleAnalyticsOperationSpec, } from './google-analytics-operations';
39
+ export { NOTION_OPERATIONS, NOTION_OPERATION_SPECS, NOTION_DROPDOWN_OPERATIONS, isNotionOperation, } from './notion-operations';
40
+ export type { NotionOperation, NotionParamType, NotionParamSpec, NotionOperationSpec, } from './notion-operations';
37
41
  export { POSTGRES_OPERATIONS, POSTGRES_MODES, POSTGRES_OPERATION_SPECS, isPostgresOperation, } from './postgres-operations';
38
42
  export type { PostgresOperation, PostgresMode, PostgresOperationSpec, } from './postgres-operations';
39
43
  export { MONGO_OPERATIONS, MONGO_OPERATION_SPECS, isMongoOperation, } from './mongo-operations';
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DRIVE_OPERATION_SPECS = exports.DRIVE_OPERATIONS = exports.GOOGLE_CALENDAR_TOOLKIT_BY_TOOL_NAME = exports.GOOGLE_CALENDAR_TOOLKIT_SPECS = exports.isGoogleCalendarOperation = exports.GOOGLE_CALENDAR_OPERATION_SPECS = exports.GOOGLE_CALENDAR_OPERATIONS = exports.isGmailOperation = exports.resolveGmailSendFields = exports.GMAIL_SEND_LEGACY_FIELDS = exports.NATIVE_EMAIL_TOOLKIT_BY_TOOL_NAME = exports.GMAIL_TOOLKIT_BY_TOOL_NAME = exports.NATIVE_EMAIL_TOOLKIT_SPECS = exports.GMAIL_SEND_AND_WAIT_TOOL_SPEC = exports.GMAIL_ALL_TOOLKIT_SPECS = exports.GMAIL_TOOLKIT_SPECS = exports.GMAIL_TOOLKIT_OPERATIONS = exports.GMAIL_DROPDOWN_OPERATIONS = exports.GMAIL_OPERATION_GROUPS = exports.GMAIL_OPERATION_SPECS = exports.GMAIL_OPERATIONS = exports.versionCatalogErrors = exports.fieldsLost = exports.fieldsLostBetween = exports.currentVersion = exports.versionSpec = exports.versionsOf = exports.isVersioned = exports.NODE_TYPE_TO_PREFIX = exports.PREFIX_TO_NODE_TYPE = exports.NODE_STATE_KEYS = exports.NODE_COLORS = exports.NODE_DETAIL_PATHS = exports.getNodeRegistryEntry = exports.NODE_REGISTRY = exports.getNodeDispatchConfig = exports.getAllNodeCollections = exports.NODE_DISPATCH = exports.resolveNodeId = exports.PREFIX_TO_TYPE = exports.NODE_UI = exports.ALL_NODE_TYPES = exports.isNodeType = exports.isTerminal = exports.canSendToNodes = exports.canReceiveFromNodes = exports.canReceiveFrom = exports.NODE_CONNECTIONS = exports.iterableMeta = exports.singleMeta = void 0;
4
- exports.getModelsFor = exports.MODEL_CONTEXT_WINDOWS = exports.LLM_MODELS = exports.LLM_PROVIDERS = exports.DOCS_TOOLKIT_DEFAULTABLE = exports.DOCS_TOOLKIT_BY_TOOL_NAME = exports.DOCS_TOOLKIT_SPECS = exports.isDocsOperation = exports.DOCS_OPERATION_SPECS = exports.DOCS_OPERATIONS = exports.isMongoOperation = exports.MONGO_OPERATION_SPECS = exports.MONGO_OPERATIONS = exports.isPostgresOperation = exports.POSTGRES_OPERATION_SPECS = exports.POSTGRES_MODES = exports.POSTGRES_OPERATIONS = exports.isGoogleAnalyticsOperation = exports.GOOGLE_ANALYTICS_DROPDOWN_OPERATIONS = exports.GOOGLE_ANALYTICS_OPERATION_SPECS = exports.GOOGLE_ANALYTICS_OPERATIONS = exports.isGoogleContactsOperation = exports.GOOGLE_CONTACTS_DEFAULT_PERSON_FIELDS = exports.GOOGLE_CONTACTS_OPERATION_GROUPS = exports.GOOGLE_CONTACTS_OPERATION_SPECS = exports.GOOGLE_CONTACTS_OPERATIONS_V2 = exports.GOOGLE_CONTACTS_OPERATIONS_V1 = exports.GOOGLE_CONTACTS_OPERATIONS = exports.SHEETS_TOOLKIT_DEFAULTABLE = exports.SHEETS_TOOLKIT_BY_TOOL_NAME = exports.SHEETS_TOOLKIT_SPECS = exports.isSheetsOperation = exports.SHEETS_OPERATION_SPECS = exports.SHEETS_OPERATIONS = exports.isSlackOperation = exports.SLACK_OPERATION_SPECS = exports.SLACK_OPERATIONS = exports.isDiscordOperation = exports.DISCORD_OPERATION_SPECS = exports.DISCORD_OPERATIONS = exports.isWhatsAppOperation = exports.WHATSAPP_OPERATIONS = exports.TELEGRAM_TOOLKIT_BY_TOOL_NAME = exports.TELEGRAM_TOOLKIT_SPECS = exports.isTelegramOperation = exports.TELEGRAM_OPERATION_SPECS = exports.TELEGRAM_OPERATIONS = exports.DRIVE_TOOLKIT_BY_TOOL_NAME = exports.DRIVE_TOOLKIT_SPECS = exports.isDriveOperation = void 0;
5
- exports.isCredentialType = exports.getCredentialType = exports.credentialTypeValues = exports.CREDENTIAL_TYPE_VALUES = exports.CREDENTIAL_TYPES = exports.getModelLabel = exports.getDefaultModel = void 0;
4
+ exports.DOCS_OPERATION_SPECS = exports.DOCS_OPERATIONS = exports.isMongoOperation = exports.MONGO_OPERATION_SPECS = exports.MONGO_OPERATIONS = exports.isPostgresOperation = exports.POSTGRES_OPERATION_SPECS = exports.POSTGRES_MODES = exports.POSTGRES_OPERATIONS = exports.isNotionOperation = exports.NOTION_DROPDOWN_OPERATIONS = exports.NOTION_OPERATION_SPECS = exports.NOTION_OPERATIONS = exports.isGoogleAnalyticsOperation = exports.GOOGLE_ANALYTICS_DROPDOWN_OPERATIONS = exports.GOOGLE_ANALYTICS_OPERATION_SPECS = exports.GOOGLE_ANALYTICS_OPERATIONS = exports.isGoogleContactsOperation = exports.GOOGLE_CONTACTS_DEFAULT_PERSON_FIELDS = exports.GOOGLE_CONTACTS_OPERATION_GROUPS = exports.GOOGLE_CONTACTS_OPERATION_SPECS = exports.GOOGLE_CONTACTS_OPERATIONS_V2 = exports.GOOGLE_CONTACTS_OPERATIONS_V1 = exports.GOOGLE_CONTACTS_OPERATIONS = exports.SHEETS_TOOLKIT_DEFAULTABLE = exports.SHEETS_TOOLKIT_BY_TOOL_NAME = exports.SHEETS_TOOLKIT_SPECS = exports.isSheetsOperation = exports.SHEETS_OPERATION_SPECS = exports.SHEETS_OPERATIONS = exports.isSlackOperation = exports.SLACK_OPERATION_SPECS = exports.SLACK_OPERATIONS = exports.isMailchimpOperation = exports.MAILCHIMP_CONTACT_STATUSES = exports.MAILCHIMP_OPERATION_SPECS = exports.MAILCHIMP_OPERATIONS = exports.isDiscordOperation = exports.DISCORD_OPERATION_SPECS = exports.DISCORD_OPERATIONS = exports.isWhatsAppOperation = exports.WHATSAPP_OPERATIONS = exports.TELEGRAM_TOOLKIT_BY_TOOL_NAME = exports.TELEGRAM_TOOLKIT_SPECS = exports.isTelegramOperation = exports.TELEGRAM_OPERATION_SPECS = exports.TELEGRAM_OPERATIONS = exports.DRIVE_TOOLKIT_BY_TOOL_NAME = exports.DRIVE_TOOLKIT_SPECS = exports.isDriveOperation = void 0;
5
+ exports.isCredentialType = exports.getCredentialType = exports.credentialTypeValues = exports.CREDENTIAL_TYPE_VALUES = exports.CREDENTIAL_TYPES = exports.getModelLabel = exports.getDefaultModel = exports.getModelsFor = exports.MODEL_CONTEXT_WINDOWS = exports.LLM_MODELS = exports.LLM_PROVIDERS = exports.DOCS_TOOLKIT_DEFAULTABLE = exports.DOCS_TOOLKIT_BY_TOOL_NAME = exports.DOCS_TOOLKIT_SPECS = exports.isDocsOperation = void 0;
6
6
  var types_1 = require("./types");
7
7
  Object.defineProperty(exports, "singleMeta", { enumerable: true, get: function () { return types_1.singleMeta; } });
8
8
  Object.defineProperty(exports, "iterableMeta", { enumerable: true, get: function () { return types_1.iterableMeta; } });
@@ -87,6 +87,11 @@ var discord_operations_1 = require("./discord-operations");
87
87
  Object.defineProperty(exports, "DISCORD_OPERATIONS", { enumerable: true, get: function () { return discord_operations_1.DISCORD_OPERATIONS; } });
88
88
  Object.defineProperty(exports, "DISCORD_OPERATION_SPECS", { enumerable: true, get: function () { return discord_operations_1.DISCORD_OPERATION_SPECS; } });
89
89
  Object.defineProperty(exports, "isDiscordOperation", { enumerable: true, get: function () { return discord_operations_1.isDiscordOperation; } });
90
+ var mailchimp_operations_1 = require("./mailchimp-operations");
91
+ Object.defineProperty(exports, "MAILCHIMP_OPERATIONS", { enumerable: true, get: function () { return mailchimp_operations_1.MAILCHIMP_OPERATIONS; } });
92
+ Object.defineProperty(exports, "MAILCHIMP_OPERATION_SPECS", { enumerable: true, get: function () { return mailchimp_operations_1.MAILCHIMP_OPERATION_SPECS; } });
93
+ Object.defineProperty(exports, "MAILCHIMP_CONTACT_STATUSES", { enumerable: true, get: function () { return mailchimp_operations_1.MAILCHIMP_CONTACT_STATUSES; } });
94
+ Object.defineProperty(exports, "isMailchimpOperation", { enumerable: true, get: function () { return mailchimp_operations_1.isMailchimpOperation; } });
90
95
  var slack_operations_1 = require("./slack-operations");
91
96
  Object.defineProperty(exports, "SLACK_OPERATIONS", { enumerable: true, get: function () { return slack_operations_1.SLACK_OPERATIONS; } });
92
97
  Object.defineProperty(exports, "SLACK_OPERATION_SPECS", { enumerable: true, get: function () { return slack_operations_1.SLACK_OPERATION_SPECS; } });
@@ -112,6 +117,11 @@ Object.defineProperty(exports, "GOOGLE_ANALYTICS_OPERATIONS", { enumerable: true
112
117
  Object.defineProperty(exports, "GOOGLE_ANALYTICS_OPERATION_SPECS", { enumerable: true, get: function () { return google_analytics_operations_1.GOOGLE_ANALYTICS_OPERATION_SPECS; } });
113
118
  Object.defineProperty(exports, "GOOGLE_ANALYTICS_DROPDOWN_OPERATIONS", { enumerable: true, get: function () { return google_analytics_operations_1.GOOGLE_ANALYTICS_DROPDOWN_OPERATIONS; } });
114
119
  Object.defineProperty(exports, "isGoogleAnalyticsOperation", { enumerable: true, get: function () { return google_analytics_operations_1.isGoogleAnalyticsOperation; } });
120
+ var notion_operations_1 = require("./notion-operations");
121
+ Object.defineProperty(exports, "NOTION_OPERATIONS", { enumerable: true, get: function () { return notion_operations_1.NOTION_OPERATIONS; } });
122
+ Object.defineProperty(exports, "NOTION_OPERATION_SPECS", { enumerable: true, get: function () { return notion_operations_1.NOTION_OPERATION_SPECS; } });
123
+ Object.defineProperty(exports, "NOTION_DROPDOWN_OPERATIONS", { enumerable: true, get: function () { return notion_operations_1.NOTION_DROPDOWN_OPERATIONS; } });
124
+ Object.defineProperty(exports, "isNotionOperation", { enumerable: true, get: function () { return notion_operations_1.isNotionOperation; } });
115
125
  var postgres_operations_1 = require("./postgres-operations");
116
126
  Object.defineProperty(exports, "POSTGRES_OPERATIONS", { enumerable: true, get: function () { return postgres_operations_1.POSTGRES_OPERATIONS; } });
117
127
  Object.defineProperty(exports, "POSTGRES_MODES", { enumerable: true, get: function () { return postgres_operations_1.POSTGRES_MODES; } });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Mailchimp Marketing API operations — single source of truth across the
3
+ * api, the dashboard and downstream consumers (MCP server).
4
+ *
5
+ * ## Three, out of one hundred and eighty-one
6
+ *
7
+ * The Marketing API has 181 routes. This node exposes three, and the choice
8
+ * is argued in `api/docs/ADR-0003-mailchimp.md`: a menu of 181 is how a node
9
+ * becomes unusable, and these three cover what people actually automate —
10
+ * put someone in a list, tag them, drop them into a journey.
11
+ *
12
+ * Campaign creation and sending were considered and left out of v1: a
13
+ * campaign needs a template, a segment and a schedule, which is a
14
+ * configuration surface of its own and not something anyone drives from a
15
+ * webhook on day one.
16
+ *
17
+ * ## Every field here exists in Mailchimp's OpenAPI schema
18
+ *
19
+ * Taken from `api.mailchimp.com/schema/3.0/Swagger.json`, not from memory.
20
+ * If you add a parameter, check it there first — an invented field is
21
+ * accepted, ignored, and then read back by the next maintainer as if it did
22
+ * something.
23
+ */
24
+ export declare const MAILCHIMP_OPERATIONS: readonly ["upsertContact", "setTags", "triggerJourney"];
25
+ export type MailchimpOperation = (typeof MAILCHIMP_OPERATIONS)[number];
26
+ /** Type guard — for DTOs and AI tool calls, where the input is untrusted. */
27
+ export declare function isMailchimpOperation(value: unknown): value is MailchimpOperation;
28
+ /**
29
+ * Contact status. `pending` is the default everywhere on purpose.
30
+ *
31
+ * Writing `subscribed` for someone who never consented is a legal problem,
32
+ * not a technical one — and a default of `subscribed` is exactly what makes
33
+ * it easy. `pending` sends Mailchimp's confirmation email, which is the
34
+ * behaviour someone should have to opt *out* of.
35
+ */
36
+ export declare const MAILCHIMP_CONTACT_STATUSES: readonly ["pending", "subscribed", "unsubscribed", "cleaned", "transactional"];
37
+ export type MailchimpContactStatus = (typeof MAILCHIMP_CONTACT_STATUSES)[number];
38
+ export interface MailchimpParamSpec {
39
+ /** Field key — also the property name on operationConfig. */
40
+ name: string;
41
+ label: string;
42
+ /** Param type, so the dashboard renders the right control. `audience`
43
+ * and `journeyStep` are pickers backed by live Mailchimp lookups. */
44
+ type: 'audience' | 'journeyStep' | 'email' | 'string' | 'status' | 'tags' | 'json' | 'boolean';
45
+ required?: boolean;
46
+ description: string;
47
+ placeholder?: string;
48
+ }
49
+ export interface MailchimpOperationSpec {
50
+ label: string;
51
+ description: string;
52
+ /** The Marketing API route this maps to, so the mapping is checkable. */
53
+ apiRoute: string;
54
+ params: MailchimpParamSpec[];
55
+ }
56
+ export declare const MAILCHIMP_OPERATION_SPECS: Record<MailchimpOperation, MailchimpOperationSpec>;
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ /**
3
+ * Mailchimp Marketing API operations — single source of truth across the
4
+ * api, the dashboard and downstream consumers (MCP server).
5
+ *
6
+ * ## Three, out of one hundred and eighty-one
7
+ *
8
+ * The Marketing API has 181 routes. This node exposes three, and the choice
9
+ * is argued in `api/docs/ADR-0003-mailchimp.md`: a menu of 181 is how a node
10
+ * becomes unusable, and these three cover what people actually automate —
11
+ * put someone in a list, tag them, drop them into a journey.
12
+ *
13
+ * Campaign creation and sending were considered and left out of v1: a
14
+ * campaign needs a template, a segment and a schedule, which is a
15
+ * configuration surface of its own and not something anyone drives from a
16
+ * webhook on day one.
17
+ *
18
+ * ## Every field here exists in Mailchimp's OpenAPI schema
19
+ *
20
+ * Taken from `api.mailchimp.com/schema/3.0/Swagger.json`, not from memory.
21
+ * If you add a parameter, check it there first — an invented field is
22
+ * accepted, ignored, and then read back by the next maintainer as if it did
23
+ * something.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.MAILCHIMP_OPERATION_SPECS = exports.MAILCHIMP_CONTACT_STATUSES = exports.MAILCHIMP_OPERATIONS = void 0;
27
+ exports.isMailchimpOperation = isMailchimpOperation;
28
+ exports.MAILCHIMP_OPERATIONS = [
29
+ 'upsertContact',
30
+ 'setTags',
31
+ 'triggerJourney',
32
+ ];
33
+ /** Type guard — for DTOs and AI tool calls, where the input is untrusted. */
34
+ function isMailchimpOperation(value) {
35
+ return (typeof value === 'string' &&
36
+ exports.MAILCHIMP_OPERATIONS.includes(value));
37
+ }
38
+ /**
39
+ * Contact status. `pending` is the default everywhere on purpose.
40
+ *
41
+ * Writing `subscribed` for someone who never consented is a legal problem,
42
+ * not a technical one — and a default of `subscribed` is exactly what makes
43
+ * it easy. `pending` sends Mailchimp's confirmation email, which is the
44
+ * behaviour someone should have to opt *out* of.
45
+ */
46
+ exports.MAILCHIMP_CONTACT_STATUSES = [
47
+ 'pending',
48
+ 'subscribed',
49
+ 'unsubscribed',
50
+ 'cleaned',
51
+ 'transactional',
52
+ ];
53
+ exports.MAILCHIMP_OPERATION_SPECS = {
54
+ upsertContact: {
55
+ label: 'Add or update contact',
56
+ description: 'Add a contact to an audience, or update them if the email is already there.',
57
+ apiRoute: 'PUT /lists/{list_id}/members/{subscriber_hash}',
58
+ params: [
59
+ {
60
+ name: 'audienceId',
61
+ label: 'Audience',
62
+ type: 'audience',
63
+ required: true,
64
+ description: 'The audience the contact belongs to.',
65
+ },
66
+ {
67
+ name: 'email',
68
+ label: 'Email address',
69
+ type: 'email',
70
+ required: true,
71
+ description: 'The contact. Mailchimp accepts the address itself here, so no hashing is needed.',
72
+ placeholder: '{{payload.email}}',
73
+ },
74
+ {
75
+ name: 'statusIfNew',
76
+ label: 'Status if new',
77
+ type: 'status',
78
+ required: true,
79
+ description: 'Only applies when the contact does not exist yet. An existing contact keeps the status it already had.',
80
+ },
81
+ {
82
+ name: 'mergeFields',
83
+ label: 'Merge fields',
84
+ type: 'json',
85
+ description: 'Mailchimp merge tags, e.g. {"FNAME":"Ada","LNAME":"Lovelace"}. The tags are defined per audience.',
86
+ placeholder: '{\n "FNAME": "{{payload.firstName}}"\n}',
87
+ },
88
+ {
89
+ name: 'language',
90
+ label: 'Language',
91
+ type: 'string',
92
+ description: 'Two-letter language code, e.g. "es". Optional.',
93
+ },
94
+ {
95
+ name: 'vip',
96
+ label: 'Mark as VIP',
97
+ type: 'boolean',
98
+ description: 'Mailchimp caps VIP contacts at 5,000 per audience.',
99
+ },
100
+ ],
101
+ },
102
+ setTags: {
103
+ label: 'Add or remove tags',
104
+ description: 'Set tags on a contact. Adding and removing happen in the same call.',
105
+ apiRoute: 'POST /lists/{list_id}/members/{subscriber_hash}/tags',
106
+ params: [
107
+ {
108
+ name: 'audienceId',
109
+ label: 'Audience',
110
+ type: 'audience',
111
+ required: true,
112
+ description: 'Tags are scoped to an audience, not to the account.',
113
+ },
114
+ {
115
+ name: 'email',
116
+ label: 'Email address',
117
+ type: 'email',
118
+ required: true,
119
+ description: 'The contact to tag.',
120
+ placeholder: '{{payload.email}}',
121
+ },
122
+ {
123
+ name: 'tags',
124
+ label: 'Tags',
125
+ type: 'tags',
126
+ required: true,
127
+ description: 'Each tag is set active (add) or inactive (remove). One call can do both.',
128
+ },
129
+ {
130
+ name: 'isSyncing',
131
+ label: 'Skip automations',
132
+ type: 'boolean',
133
+ description: 'When on, automations that watch these tags do NOT fire. Use it for a bulk backfill you do not want to trigger emails.',
134
+ },
135
+ ],
136
+ },
137
+ triggerJourney: {
138
+ label: 'Add contact to a journey',
139
+ description: 'Drop a contact into a Customer Journey at a specific step.',
140
+ apiRoute: 'POST /customer-journeys/journeys/{journey_id}/steps/{step_id}/actions/trigger',
141
+ params: [
142
+ {
143
+ name: 'journeyStep',
144
+ label: 'Journey step',
145
+ type: 'journeyStep',
146
+ required: true,
147
+ description: 'The journey and the step inside it. The journey must already be running in Mailchimp.',
148
+ },
149
+ {
150
+ name: 'email',
151
+ label: 'Email address',
152
+ type: 'email',
153
+ required: true,
154
+ description: 'The contact to enrol. They must already exist in the journey audience — this operation does not create them.',
155
+ placeholder: '{{payload.email}}',
156
+ },
157
+ ],
158
+ },
159
+ };
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Notion operation enum — single source of truth across the API, Dashboard y
3
+ * Message Broker. Lo usan:
4
+ * - notionAction entity / DTO (api): campo `operation` + validación
5
+ * - la página de detalle del dashboard
6
+ * - specs de toolkit MCP, si algún día el nodo gana modo herramienta
7
+ *
8
+ * Mismo patrón que `google-analytics-operations.ts` y `drive-operations.ts`.
9
+ *
10
+ * ── Contra qué versión está escrito ──
11
+ *
12
+ * `Notion-Version: 2026-03-11`. Importa más de lo normal porque Notion movió
13
+ * cosas de sitio dos veces seguidas y los ejemplos que hay por internet están
14
+ * casi todos contra la API anterior:
15
+ *
16
+ * - 2025-09-03 partió `database` en database + **data source**. El query
17
+ * dejó de vivir en `databases/{id}/query` y pasó a
18
+ * `data_sources/{id}/query`. Una database contiene N data sources.
19
+ * - 2026-03-11 quitó el alias `archived` (ahora es **`in_trash`**), cambió
20
+ * el `after` de append blocks por un objeto **`position`**, y renombró el
21
+ * bloque `transcription` a `meeting_notes`.
22
+ *
23
+ * ── Por qué hay operaciones de markdown ──
24
+ *
25
+ * Escribir contenido en Notion era construir un árbol de bloques a mano, que
26
+ * es con diferencia lo más pesado de su API. Desde 2026-03-11 hay
27
+ * `GET/PATCH /v1/pages/{id}/markdown`, disponible —lo dicen los docs con esas
28
+ * palabras— para "all connection types (public, internal, and personal access
29
+ * tokens)". Con eso, escribir una página es mandar texto.
30
+ *
31
+ * Y el markdown cubre también el añadir-sin-pisar, vía el comando
32
+ * `insert_content` con `position: end`. `PATCH /v1/blocks/{id}/children` NO
33
+ * acepta markdown —sólo objetos de bloque—, así que la única op de bloques que
34
+ * queda es la de LEER la estructura cruda, que es lo que markdown no da.
35
+ *
36
+ * ── Lo que NO es una operación ──
37
+ *
38
+ * Listar las databases/data sources a las que la credencial tiene acceso, y
39
+ * leer el esquema para pintar un selector, NO son ops del nodo: son endpoints
40
+ * del controlador, como Drive alimenta su `DriveFilePicker`. Una op existe
41
+ * para producir payload en una corrida; esto otro es para rellenar el
42
+ * formulario.
43
+ *
44
+ * ── Decisiones propias de Notion (anti-n8n) ──
45
+ *
46
+ * - La database/data source y la página se ELIGEN de una lista viva, no se
47
+ * teclean. Un id de Notion es un UUID sin guiones dentro de una URL larga;
48
+ * pedirle eso al usuario es pedirle que se equivoque.
49
+ * - Las propiedades de una página van en JSON crudo salvo el título, que es
50
+ * el 90% de los casos y tiene campo propio. Cada tipo de propiedad de
51
+ * Notion tiene una forma distinta (`rich_text`, `select`, `date`,
52
+ * `relation`…) y fingir que caben en tres campos genéricos produce un 400
53
+ * que no explica nada.
54
+ * - Ninguna op declarada sin implementar. El enum ES el contrato de la
55
+ * pantalla: una op declarada se pinta en el desplegable y falla al
56
+ * ejecutarse. Entran cuando entre su código.
57
+ */
58
+ export declare const NOTION_OPERATIONS: readonly ["createPage", "getPage", "getPageMarkdown", "updatePageProperties", "updatePageMarkdown", "archivePage", "queryDataSource", "getDataSource", "appendToPage", "getBlockChildren", "search", "createComment"];
59
+ export type NotionOperation = (typeof NOTION_OPERATIONS)[number];
60
+ /** Type guard — para validar entrada no fiable (DTOs, tool calls). */
61
+ export declare function isNotionOperation(value: unknown): value is NotionOperation;
62
+ export type NotionParamType =
63
+ /** Selector vivo de data sources accesibles (necesita credencial). */
64
+ 'notionDataSource'
65
+ /** Selector vivo de páginas accesibles; admite pegar un id o una URL. */
66
+ | 'notionPage'
67
+ /** Cualquier bloque contenedor: una página vale como bloque. */
68
+ | 'notionBlock'
69
+ /** Texto con autocompletado de `{{payload.*}}`. */
70
+ | 'template'
71
+ /** Área de texto larga, también con autocompletado. */
72
+ | 'textarea'
73
+ /** `<select>` normal; las etiquetas no son los valores, `options` obliga. */
74
+ | 'select' | 'number'
75
+ /**
76
+ * `<select>` de `''` / `'true'` / `'false'` que se guarda como BOOLEANO, o
77
+ * no se guarda si es `''`. "— sin tocar —" tiene que significar que la
78
+ * clave no viaja, no que viaja en `false`.
79
+ */
80
+ | 'booleanSelect'
81
+ /** Área de texto para JSON crudo (propiedades, filtros, orden). */
82
+ | 'json';
83
+ export interface NotionParamSpec {
84
+ /** La clave de `operationConfig`. Es lo que lee la api, no un nombre de UI. */
85
+ name: string;
86
+ label: string;
87
+ type: NotionParamType;
88
+ required?: boolean;
89
+ /** Ayuda bajo el campo. Texto plano: el paquete no lleva React. */
90
+ description?: string;
91
+ placeholder?: string;
92
+ /** Prerrellenado cuando la config guardada no trae valor para esta clave. */
93
+ default?: string | number;
94
+ /** Obligatorio en `select` y `booleanSelect` — etiqueta y valor difieren. */
95
+ options?: ReadonlyArray<{
96
+ value: string;
97
+ label: string;
98
+ }>;
99
+ min?: number;
100
+ max?: number;
101
+ /** Se pinta en la pestaña "Advanced" en vez de en "Config". */
102
+ advanced?: boolean;
103
+ /** Sólo visible cuando un campo hermano vale uno de estos. */
104
+ showWhen?: {
105
+ field: string;
106
+ in: ReadonlyArray<string>;
107
+ };
108
+ }
109
+ export interface NotionOperationSpec {
110
+ /** Etiqueta de la fila en el desplegable de operación. */
111
+ label: string;
112
+ /** Sub-línea bajo la etiqueta. */
113
+ description: string;
114
+ /** Endpoint al que mapea, tal cual, para poder cotejarlo con los docs. */
115
+ apiMethod: string;
116
+ /** Esquema de parámetros. El orden ES el orden en pantalla. */
117
+ params: NotionParamSpec[];
118
+ }
119
+ export declare const NOTION_OPERATION_SPECS: Record<NotionOperation, NotionOperationSpec>;
120
+ /**
121
+ * Las operaciones tal y como se listan en el desplegable. Hoy es el enum
122
+ * entero; existe para que añadir una op interna (sin fila propia) no obligue
123
+ * a que la página vuelva a decidir por su cuenta qué enseñar.
124
+ */
125
+ export declare const NOTION_DROPDOWN_OPERATIONS: readonly NotionOperation[];
@@ -0,0 +1,515 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NOTION_DROPDOWN_OPERATIONS = exports.NOTION_OPERATION_SPECS = exports.NOTION_OPERATIONS = void 0;
4
+ exports.isNotionOperation = isNotionOperation;
5
+ /**
6
+ * Notion operation enum — single source of truth across the API, Dashboard y
7
+ * Message Broker. Lo usan:
8
+ * - notionAction entity / DTO (api): campo `operation` + validación
9
+ * - la página de detalle del dashboard
10
+ * - specs de toolkit MCP, si algún día el nodo gana modo herramienta
11
+ *
12
+ * Mismo patrón que `google-analytics-operations.ts` y `drive-operations.ts`.
13
+ *
14
+ * ── Contra qué versión está escrito ──
15
+ *
16
+ * `Notion-Version: 2026-03-11`. Importa más de lo normal porque Notion movió
17
+ * cosas de sitio dos veces seguidas y los ejemplos que hay por internet están
18
+ * casi todos contra la API anterior:
19
+ *
20
+ * - 2025-09-03 partió `database` en database + **data source**. El query
21
+ * dejó de vivir en `databases/{id}/query` y pasó a
22
+ * `data_sources/{id}/query`. Una database contiene N data sources.
23
+ * - 2026-03-11 quitó el alias `archived` (ahora es **`in_trash`**), cambió
24
+ * el `after` de append blocks por un objeto **`position`**, y renombró el
25
+ * bloque `transcription` a `meeting_notes`.
26
+ *
27
+ * ── Por qué hay operaciones de markdown ──
28
+ *
29
+ * Escribir contenido en Notion era construir un árbol de bloques a mano, que
30
+ * es con diferencia lo más pesado de su API. Desde 2026-03-11 hay
31
+ * `GET/PATCH /v1/pages/{id}/markdown`, disponible —lo dicen los docs con esas
32
+ * palabras— para "all connection types (public, internal, and personal access
33
+ * tokens)". Con eso, escribir una página es mandar texto.
34
+ *
35
+ * Y el markdown cubre también el añadir-sin-pisar, vía el comando
36
+ * `insert_content` con `position: end`. `PATCH /v1/blocks/{id}/children` NO
37
+ * acepta markdown —sólo objetos de bloque—, así que la única op de bloques que
38
+ * queda es la de LEER la estructura cruda, que es lo que markdown no da.
39
+ *
40
+ * ── Lo que NO es una operación ──
41
+ *
42
+ * Listar las databases/data sources a las que la credencial tiene acceso, y
43
+ * leer el esquema para pintar un selector, NO son ops del nodo: son endpoints
44
+ * del controlador, como Drive alimenta su `DriveFilePicker`. Una op existe
45
+ * para producir payload en una corrida; esto otro es para rellenar el
46
+ * formulario.
47
+ *
48
+ * ── Decisiones propias de Notion (anti-n8n) ──
49
+ *
50
+ * - La database/data source y la página se ELIGEN de una lista viva, no se
51
+ * teclean. Un id de Notion es un UUID sin guiones dentro de una URL larga;
52
+ * pedirle eso al usuario es pedirle que se equivoque.
53
+ * - Las propiedades de una página van en JSON crudo salvo el título, que es
54
+ * el 90% de los casos y tiene campo propio. Cada tipo de propiedad de
55
+ * Notion tiene una forma distinta (`rich_text`, `select`, `date`,
56
+ * `relation`…) y fingir que caben en tres campos genéricos produce un 400
57
+ * que no explica nada.
58
+ * - Ninguna op declarada sin implementar. El enum ES el contrato de la
59
+ * pantalla: una op declarada se pinta en el desplegable y falla al
60
+ * ejecutarse. Entran cuando entre su código.
61
+ */
62
+ exports.NOTION_OPERATIONS = [
63
+ // Páginas
64
+ 'createPage', // POST /v1/pages
65
+ 'getPage', // GET /v1/pages/{id}
66
+ 'getPageMarkdown', // GET /v1/pages/{id}/markdown
67
+ 'updatePageProperties', // PATCH /v1/pages/{id}
68
+ 'updatePageMarkdown', // PATCH /v1/pages/{id}/markdown
69
+ 'archivePage', // PATCH /v1/pages/{id} { in_trash: true }
70
+ // Data sources (lo que antes era "la database")
71
+ 'queryDataSource', // POST /v1/data_sources/{id}/query
72
+ 'getDataSource', // GET /v1/data_sources/{id}
73
+ // Bloques
74
+ 'appendToPage', // PATCH /v1/pages/{id}/markdown (insert_content)
75
+ 'getBlockChildren', // GET /v1/blocks/{id}/children
76
+ // Transversales
77
+ 'search', // POST /v1/search
78
+ 'createComment', // POST /v1/comments
79
+ ];
80
+ /** Type guard — para validar entrada no fiable (DTOs, tool calls). */
81
+ function isNotionOperation(value) {
82
+ return (typeof value === 'string' &&
83
+ exports.NOTION_OPERATIONS.includes(value));
84
+ }
85
+ /* Fábricas de los campos que se repiten, para que un cambio de copy no haya
86
+ que perseguirlo por doce operaciones. */
87
+ const pageId = (label = 'Page') => ({
88
+ name: 'pageId',
89
+ label,
90
+ type: 'notionPage',
91
+ required: true,
92
+ description: 'Pick a page the connection can see, or paste a page URL or id — a templated value like {{payload.pageId}} works too. Notion only shows pages that were explicitly shared with this connection.',
93
+ });
94
+ const dataSourceId = () => ({
95
+ name: 'dataSourceId',
96
+ label: 'Data source',
97
+ type: 'notionDataSource',
98
+ required: true,
99
+ description: 'Since API version 2025-09-03 a database holds one or more data sources, and rows live in the data source — not in the database. Most databases have exactly one.',
100
+ });
101
+ /** El JSON de propiedades, con el aviso que ahorra el 400 más común. */
102
+ const propertiesJson = () => ({
103
+ name: 'properties',
104
+ label: 'Properties (JSON)',
105
+ type: 'json',
106
+ advanced: true,
107
+ placeholder: '{ "Status": { "select": { "name": "In progress" } }, "Due": { "date": { "start": "2026-09-01" } } }',
108
+ description: 'Raw Notion property values, keyed by property name. Every property type has its own shape (rich_text, select, date, relation, …) — check the data source schema. Leave empty to only set the title.',
109
+ });
110
+ const notionVersionNote = 'Runs against Notion-Version 2026-03-11.';
111
+ exports.NOTION_OPERATION_SPECS = {
112
+ createPage: {
113
+ label: 'Create page',
114
+ description: 'A new page — as a row in a data source, or under another page.',
115
+ apiMethod: 'POST /v1/pages',
116
+ params: [
117
+ {
118
+ name: 'parentType',
119
+ label: 'Create it in',
120
+ type: 'select',
121
+ required: true,
122
+ default: 'data_source',
123
+ options: [
124
+ { value: 'data_source', label: 'A data source (adds a row)' },
125
+ { value: 'page', label: 'Another page (adds a subpage)' },
126
+ ],
127
+ description: 'A row in a database IS a page in Notion — same object, different parent.',
128
+ },
129
+ {
130
+ ...dataSourceId(),
131
+ showWhen: { field: 'parentType', in: ['data_source'] },
132
+ },
133
+ {
134
+ ...pageId('Parent page'),
135
+ name: 'parentPageId',
136
+ showWhen: { field: 'parentType', in: ['page'] },
137
+ },
138
+ {
139
+ name: 'title',
140
+ label: 'Title',
141
+ type: 'template',
142
+ required: true,
143
+ placeholder: '{{payload.subject}}',
144
+ description: 'Goes into the title property, whatever it is called in this data source.',
145
+ },
146
+ {
147
+ name: 'markdown',
148
+ label: 'Content (Markdown)',
149
+ type: 'textarea',
150
+ placeholder: '## Summary\n\n{{payload.body}}',
151
+ description: 'The page body, as Notion-flavored Markdown. This is the shortcut that avoids building a block tree by hand.',
152
+ },
153
+ {
154
+ name: 'icon',
155
+ label: 'Icon',
156
+ type: 'template',
157
+ advanced: true,
158
+ placeholder: '🚀',
159
+ description: 'A single emoji, or an external image URL.',
160
+ },
161
+ propertiesJson(),
162
+ ],
163
+ },
164
+ getPage: {
165
+ label: 'Get page',
166
+ description: 'The page object and its properties — not its content.',
167
+ apiMethod: 'GET /v1/pages/{page_id}',
168
+ params: [
169
+ pageId(),
170
+ {
171
+ name: 'filterProperties',
172
+ label: 'Only these properties',
173
+ type: 'template',
174
+ advanced: true,
175
+ placeholder: 'title,Status,Due',
176
+ description: 'Comma-separated property names. Notion returns every property by default, which on a wide database is a lot of payload for nothing.',
177
+ },
178
+ ],
179
+ },
180
+ getPageMarkdown: {
181
+ label: 'Get page as Markdown',
182
+ description: 'The whole page content as one Markdown string.',
183
+ apiMethod: 'GET /v1/pages/{page_id}/markdown',
184
+ params: [
185
+ pageId(),
186
+ {
187
+ name: 'includeTranscript',
188
+ label: 'Include meeting transcript',
189
+ type: 'booleanSelect',
190
+ advanced: true,
191
+ options: [
192
+ { value: '', label: '— leave alone —' },
193
+ { value: 'true', label: 'Yes' },
194
+ { value: 'false', label: 'No' },
195
+ ],
196
+ description: 'Only matters on pages holding meeting notes. Transcripts can be very long.',
197
+ },
198
+ ],
199
+ },
200
+ updatePageProperties: {
201
+ label: 'Update page properties',
202
+ description: 'Change property values — status, dates, relations.',
203
+ apiMethod: 'PATCH /v1/pages/{page_id}',
204
+ params: [
205
+ pageId(),
206
+ {
207
+ name: 'title',
208
+ label: 'New title',
209
+ type: 'template',
210
+ description: 'Leave empty to keep the current title.',
211
+ },
212
+ {
213
+ ...propertiesJson(),
214
+ advanced: false,
215
+ description: 'Raw Notion property values, keyed by property name. Only the properties you list are touched; the rest keep their value. Every property type has its own shape (rich_text, select, date, relation, …).',
216
+ },
217
+ {
218
+ name: 'icon',
219
+ label: 'Icon',
220
+ type: 'template',
221
+ advanced: true,
222
+ placeholder: '✅',
223
+ description: 'A single emoji, or an external image URL.',
224
+ },
225
+ ],
226
+ },
227
+ updatePageMarkdown: {
228
+ label: 'Update page content',
229
+ description: 'Rewrite the body, or search and replace inside it.',
230
+ apiMethod: 'PATCH /v1/pages/{page_id}/markdown',
231
+ params: [
232
+ pageId(),
233
+ {
234
+ name: 'mode',
235
+ label: 'How',
236
+ type: 'select',
237
+ required: true,
238
+ default: 'replace_content',
239
+ options: [
240
+ { value: 'replace_content', label: 'Replace the whole body' },
241
+ { value: 'update_content', label: 'Find and replace' },
242
+ ],
243
+ description: 'Replacing drops everything currently on the page — including blocks this flow did not write.',
244
+ },
245
+ {
246
+ name: 'markdown',
247
+ label: 'New content (Markdown)',
248
+ type: 'textarea',
249
+ required: true,
250
+ showWhen: { field: 'mode', in: ['replace_content'] },
251
+ placeholder: '## Report\n\n{{payload.summary}}',
252
+ },
253
+ {
254
+ name: 'find',
255
+ label: 'Find',
256
+ type: 'template',
257
+ required: true,
258
+ showWhen: { field: 'mode', in: ['update_content'] },
259
+ description: 'The exact text to look for. Notion matches it literally, not as a regex.',
260
+ },
261
+ {
262
+ name: 'replace',
263
+ label: 'Replace with',
264
+ type: 'textarea',
265
+ showWhen: { field: 'mode', in: ['update_content'] },
266
+ description: 'Leave empty to delete the matched text.',
267
+ },
268
+ ],
269
+ },
270
+ archivePage: {
271
+ label: 'Archive page',
272
+ description: 'Send it to the trash, or bring it back.',
273
+ apiMethod: 'PATCH /v1/pages/{page_id}',
274
+ params: [
275
+ pageId(),
276
+ {
277
+ name: 'inTrash',
278
+ label: 'Trashed',
279
+ type: 'booleanSelect',
280
+ required: true,
281
+ default: 'true',
282
+ options: [
283
+ { value: 'true', label: 'Move to trash' },
284
+ { value: 'false', label: 'Restore from trash' },
285
+ ],
286
+ description: 'Notion calls this in_trash since API version 2026-03-11; the old archived field is gone. Trashed pages are recoverable for 30 days.',
287
+ },
288
+ ],
289
+ },
290
+ queryDataSource: {
291
+ label: 'Query data source',
292
+ description: 'Rows matching a filter, in the order you ask for.',
293
+ apiMethod: 'POST /v1/data_sources/{data_source_id}/query',
294
+ params: [
295
+ dataSourceId(),
296
+ {
297
+ name: 'filterProperty',
298
+ label: 'Filter — property',
299
+ type: 'template',
300
+ placeholder: 'Status',
301
+ description: 'The property name as it reads in Notion. Leave the three filter fields empty to get everything.',
302
+ },
303
+ {
304
+ name: 'filterCondition',
305
+ label: 'Filter — condition',
306
+ type: 'select',
307
+ default: 'equals',
308
+ options: [
309
+ { value: 'equals', label: 'equals' },
310
+ { value: 'does_not_equal', label: 'does not equal' },
311
+ { value: 'contains', label: 'contains' },
312
+ { value: 'does_not_contain', label: 'does not contain' },
313
+ { value: 'is_empty', label: 'is empty' },
314
+ { value: 'is_not_empty', label: 'is not empty' },
315
+ { value: 'after', label: 'is after (date)' },
316
+ { value: 'before', label: 'is before (date)' },
317
+ { value: 'on_or_after', label: 'is on or after (date)' },
318
+ ],
319
+ },
320
+ {
321
+ name: 'filterValue',
322
+ label: 'Filter — value',
323
+ type: 'template',
324
+ placeholder: 'In progress',
325
+ showWhen: {
326
+ field: 'filterCondition',
327
+ in: [
328
+ 'equals',
329
+ 'does_not_equal',
330
+ 'contains',
331
+ 'does_not_contain',
332
+ 'after',
333
+ 'before',
334
+ 'on_or_after',
335
+ ],
336
+ },
337
+ },
338
+ {
339
+ name: 'sortProperty',
340
+ label: 'Sort by',
341
+ type: 'template',
342
+ placeholder: 'last_edited_time',
343
+ description: 'A property name, or the built-ins created_time / last_edited_time.',
344
+ },
345
+ {
346
+ name: 'sortDirection',
347
+ label: 'Direction',
348
+ type: 'select',
349
+ default: 'descending',
350
+ options: [
351
+ { value: 'descending', label: 'Newest first' },
352
+ { value: 'ascending', label: 'Oldest first' },
353
+ ],
354
+ },
355
+ {
356
+ name: 'pageSize',
357
+ label: 'How many',
358
+ type: 'number',
359
+ default: 25,
360
+ min: 1,
361
+ max: 100,
362
+ description: 'Notion caps a single query at 100 rows. Past that it paginates, and this node returns the first page only.',
363
+ },
364
+ {
365
+ name: 'filterJson',
366
+ label: 'Filter (raw JSON)',
367
+ type: 'json',
368
+ advanced: true,
369
+ placeholder: '{ "and": [ { "property": "Status", "select": { "equals": "Done" } } ] }',
370
+ description: 'The escape hatch for anything the three fields above cannot say — compound and/or, rollups, formulas. When this is filled in, it WINS and the simple filter is ignored.',
371
+ },
372
+ ],
373
+ },
374
+ getDataSource: {
375
+ label: 'Get data source schema',
376
+ description: 'Which properties exist and what type each one is.',
377
+ apiMethod: 'GET /v1/data_sources/{data_source_id}',
378
+ params: [dataSourceId()],
379
+ },
380
+ appendToPage: {
381
+ label: 'Append content',
382
+ description: 'Add to a page without touching what is already there.',
383
+ apiMethod: 'PATCH /v1/pages/{page_id}/markdown (insert_content)',
384
+ params: [
385
+ pageId(),
386
+ {
387
+ name: 'markdown',
388
+ label: 'Content (Markdown)',
389
+ type: 'textarea',
390
+ required: true,
391
+ placeholder: '- [ ] {{payload.task}}',
392
+ description: 'Appended as new blocks. Nothing already on the page is touched — this is the safe alternative to replacing the body.',
393
+ },
394
+ {
395
+ name: 'position',
396
+ label: 'Where',
397
+ type: 'select',
398
+ default: 'end',
399
+ options: [
400
+ { value: 'end', label: 'At the end' },
401
+ { value: 'start', label: 'At the top' },
402
+ ],
403
+ },
404
+ ],
405
+ },
406
+ getBlockChildren: {
407
+ label: 'Get block children',
408
+ description: 'The raw block tree, one level down.',
409
+ apiMethod: 'GET /v1/blocks/{block_id}/children',
410
+ params: [
411
+ {
412
+ name: 'blockId',
413
+ label: 'Page or block',
414
+ type: 'notionBlock',
415
+ required: true,
416
+ description: 'For reading a whole page as text, "Get page as Markdown" is almost always the one you want instead.',
417
+ },
418
+ {
419
+ name: 'pageSize',
420
+ label: 'How many',
421
+ type: 'number',
422
+ default: 50,
423
+ min: 1,
424
+ max: 100,
425
+ },
426
+ ],
427
+ },
428
+ search: {
429
+ label: 'Search',
430
+ description: 'Find pages and data sources by title.',
431
+ apiMethod: 'POST /v1/search',
432
+ params: [
433
+ {
434
+ name: 'query',
435
+ label: 'Query',
436
+ type: 'template',
437
+ placeholder: '{{payload.customerName}}',
438
+ description: 'Matches against titles only, not page content. Empty returns everything shared with the connection.',
439
+ },
440
+ {
441
+ name: 'filterObject',
442
+ label: 'Only',
443
+ type: 'select',
444
+ default: '',
445
+ options: [
446
+ { value: '', label: 'Pages and data sources' },
447
+ { value: 'page', label: 'Pages' },
448
+ { value: 'data_source', label: 'Data sources' },
449
+ ],
450
+ },
451
+ {
452
+ name: 'sortDirection',
453
+ label: 'Sort by last edited',
454
+ type: 'select',
455
+ default: 'descending',
456
+ options: [
457
+ { value: 'descending', label: 'Newest first' },
458
+ { value: 'ascending', label: 'Oldest first' },
459
+ ],
460
+ },
461
+ {
462
+ name: 'pageSize',
463
+ label: 'How many',
464
+ type: 'number',
465
+ default: 25,
466
+ min: 1,
467
+ max: 100,
468
+ },
469
+ ],
470
+ },
471
+ createComment: {
472
+ label: 'Add comment',
473
+ description: 'A comment on a page, or a reply in an existing thread.',
474
+ apiMethod: 'POST /v1/comments',
475
+ params: [
476
+ {
477
+ name: 'target',
478
+ label: 'Comment on',
479
+ type: 'select',
480
+ required: true,
481
+ default: 'page',
482
+ options: [
483
+ { value: 'page', label: 'A page' },
484
+ { value: 'discussion', label: 'An existing thread' },
485
+ ],
486
+ },
487
+ {
488
+ ...pageId(),
489
+ showWhen: { field: 'target', in: ['page'] },
490
+ },
491
+ {
492
+ name: 'discussionId',
493
+ label: 'Discussion id',
494
+ type: 'template',
495
+ required: true,
496
+ showWhen: { field: 'target', in: ['discussion'] },
497
+ description: 'Comes from a comment.created trigger payload, or from Get page comments.',
498
+ },
499
+ {
500
+ name: 'text',
501
+ label: 'Comment',
502
+ type: 'textarea',
503
+ required: true,
504
+ placeholder: 'Handled automatically — {{payload.result}}',
505
+ description: `Plain text. ${notionVersionNote}`,
506
+ },
507
+ ],
508
+ },
509
+ };
510
+ /**
511
+ * Las operaciones tal y como se listan en el desplegable. Hoy es el enum
512
+ * entero; existe para que añadir una op interna (sin fila propia) no obligue
513
+ * a que la página vuelva a decidir por su cuenta qué enseñar.
514
+ */
515
+ exports.NOTION_DROPDOWN_OPERATIONS = exports.NOTION_OPERATIONS;
package/dist/registry.js CHANGED
@@ -265,6 +265,14 @@ exports.NODE_REGISTRY = {
265
265
  stateKey: 'firecrawlActions', allStateKey: 'allFirecrawlActions',
266
266
  color: '#f97316', testable: true,
267
267
  },
268
+ mailchimpAction: {
269
+ type: 'mailchimpAction', prefix: 'mc', label: 'Mailchimp',
270
+ group: 'Actions', detailPath: '/dashboard/mailchimp-actions', apiPath: '/mailchimp-actions',
271
+ stateKey: 'mailchimpActions', allStateKey: 'allMailchimpActions',
272
+ // Mailchimp's Cavendish yellow, darkened until it holds contrast on a
273
+ // light canvas. The brand value (#ffe01b) reads as a highlighter there.
274
+ color: '#c9a227', testable: true,
275
+ },
268
276
  telegramAction: {
269
277
  type: 'telegramAction', prefix: 'tg', label: 'Telegram Action',
270
278
  group: 'Actions', detailPath: '/dashboard/telegram-actions', apiPath: '/telegram-actions',
@@ -340,6 +348,20 @@ exports.NODE_REGISTRY = {
340
348
  MCP, sin nada que las ate-- y esa duplicacion no se paga antes de ver
341
349
  el nodo funcionando en pantalla. */
342
350
  },
351
+ notionAction: {
352
+ type: 'notionAction', prefix: 'ntn', label: 'Notion',
353
+ group: 'Actions', detailPath: '/dashboard/notion-actions', apiPath: '/notion-actions',
354
+ stateKey: 'notionActions', allStateKey: 'allNotionActions',
355
+ // Notion es monocromo de marca (su negro es #37352F), y un punto negro
356
+ // en el minimapa desaparece en modo oscuro. Este stone-500 es el gris
357
+ // CÁLIDO más cercano a esa marca, y no choca con el #94a3b8 (slate, gris
358
+ // frío) que ya llevan tres nodos: en el minimapa lo único que distingue
359
+ // a un nodo es el color.
360
+ color: '#78716c', testable: true,
361
+ /* Sin isToolOnly: la v1 no trae modo toolkit de IA. Cada toolkit vive dos
362
+ veces --specs del dashboard para el AI Node y specs de la api para
363
+ MCP-- y esa duplicación no se paga antes de ver el nodo en pantalla. */
364
+ },
343
365
  vectorStore: {
344
366
  type: 'vectorStore', prefix: 'vs', label: 'Vector Store',
345
367
  group: 'Actions', detailPath: '/dashboard/vector-store-nodes', apiPath: '/vector-store-nodes',
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** All valid node type identifiers */
2
- export type NodeType = 'webhook' | 'scheduledWorkflow' | 'chatTrigger' | 'trigger' | 'voiceAgent' | 'filter' | 'transform' | 'schemaValidator' | 'conditional' | 'delay' | 'rateLimiter' | 'aggregator' | 'cache' | 'code' | 'ai' | 'merge' | 'approval' | 'split' | 'loop' | 'markdown' | 'fileTransform' | 'limit' | 'router' | 'emailAction' | 'gmailAction' | 'httpAction' | 'mongoAction' | 'postgresAction' | 'notificationAction' | 'sheetsAction' | 'calendarAction' | 'docsAction' | 'driveAction' | 'firecrawlAction' | 'telegramAction' | 'whatsappAction' | 'discordAction' | 'slackAction' | 'googleContactsAction' | 'googleAnalyticsAction' | 'vectorStore' | 'rssAction' | 'socialMediaAction' | 'stickyNote';
2
+ export type NodeType = 'webhook' | 'scheduledWorkflow' | 'chatTrigger' | 'trigger' | 'voiceAgent' | 'filter' | 'transform' | 'schemaValidator' | 'conditional' | 'delay' | 'rateLimiter' | 'aggregator' | 'cache' | 'code' | 'ai' | 'merge' | 'approval' | 'split' | 'loop' | 'markdown' | 'fileTransform' | 'limit' | 'router' | 'emailAction' | 'gmailAction' | 'httpAction' | 'mongoAction' | 'postgresAction' | 'notificationAction' | 'sheetsAction' | 'calendarAction' | 'docsAction' | 'driveAction' | 'firecrawlAction' | 'telegramAction' | 'whatsappAction' | 'discordAction' | 'slackAction' | 'googleContactsAction' | 'googleAnalyticsAction' | 'notionAction' | 'vectorStore' | 'rssAction' | 'socialMediaAction' | 'mailchimpAction' | 'stickyNote';
3
3
  /** Node role in the pipeline */
4
4
  export type NodeRole = 'source' | 'processing' | 'flowControl' | 'routing' | 'action' | 'monitoring';
5
5
  /** Handle positions on the canvas node */
package/dist/ui.js CHANGED
@@ -75,9 +75,11 @@ exports.NODE_UI = {
75
75
  telegramAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'telegram' } },
76
76
  whatsappAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'whatsapp' } },
77
77
  discordAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'discord' } },
78
+ mailchimpAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'mailchimp' } },
78
79
  slackAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'slack' } },
79
80
  googleContactsAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true } },
80
81
  googleAnalyticsAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true } },
82
+ notionAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'notion' } },
81
83
  vectorStore: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true } },
82
84
  rssAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true } },
83
85
  socialMediaAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true } },
@@ -97,8 +99,10 @@ exports.PREFIX_TO_TYPE = [
97
99
  { prefix: 'wa-', type: 'whatsappAction', canvasType: 'whatsappAction' },
98
100
  { prefix: 'dc-', type: 'discordAction', canvasType: 'discordAction' },
99
101
  { prefix: 'sl-', type: 'slackAction', canvasType: 'slackAction' },
102
+ { prefix: 'mc-', type: 'mailchimpAction', canvasType: 'mailchimpAction' },
100
103
  { prefix: 'gc-', type: 'googleContactsAction', canvasType: 'googleContactsAction' },
101
104
  { prefix: 'gaa-', type: 'googleAnalyticsAction', canvasType: 'googleAnalyticsAction' },
105
+ { prefix: 'ntn-', type: 'notionAction', canvasType: 'notionAction' },
102
106
  { prefix: 'ha-', type: 'httpAction', canvasType: 'httpAction' },
103
107
  { prefix: 'ma-', type: 'mongoAction', canvasType: 'mongoAction' },
104
108
  { prefix: 'pg-', type: 'postgresAction', canvasType: 'postgresAction' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostwebhook/node-types",
3
- "version": "1.57.0",
3
+ "version": "1.59.0",
4
4
  "description": "Shared node type definitions, connection rules, and dispatch config for HostWebhook",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",