From b7bd29c53868130897d7a7e3568e1a3cfc3258f8 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 13:35:45 +0200 Subject: [PATCH 01/16] feat: add HookIncomings.list() --- src/endpoints/hook-incomings.ts | 74 +++++++++++++++++++++++++++++ src/endpoints/hooks.ts | 7 +++ test/hook-incomings.spec.ts | 36 ++++++++++++++ test/mocks/hook-incomings/list.json | 22 +++++++++ 4 files changed, 139 insertions(+) create mode 100644 src/endpoints/hook-incomings.ts create mode 100644 test/hook-incomings.spec.ts create mode 100644 test/mocks/hook-incomings/list.json diff --git a/src/endpoints/hook-incomings.ts b/src/endpoints/hook-incomings.ts new file mode 100644 index 0000000..e3de4cc --- /dev/null +++ b/src/endpoints/hook-incomings.ts @@ -0,0 +1,74 @@ +import type { FetchFunction, JSONValue, Pagination } from '../types.js'; + +/** + * A single item in a webhook's processing queue. + * Queue items accumulate when a webhook receives data it can't immediately + * hand off to a running scenario (e.g. because the scenario is inactive). + */ +export type HookIncoming = { + /** Unique identifier of the queue item */ + id: string; + /** Scope the item was queued under (e.g. 'hook') */ + scope: string; + /** Size of the queued payload in bytes */ + size: number; + /** ISO 8601 timestamp of when the item was queued */ + created: string; +}; + +/** + * Options for listing a webhook's queued incoming items. + */ +export type ListHookIncomingsOptions = { + /** Only include items queued at or after this Unix timestamp (ms) */ + from?: number; + /** Only include items queued at or before this Unix timestamp (ms) */ + to?: number; + /** Pagination options */ + pg?: Partial>; +}; + +/** + * Response format for listing a webhook's queued items. + */ +type ListHookIncomingsResponse = { + /** Queue items matching the query */ + incomings: HookIncoming[]; + /** Pagination information */ + pg: Partial>; +}; + +/** + * Class providing methods for working with a Make webhook's processing queue. + * Items accumulate here when a webhook receives data it can't immediately + * hand off to a running scenario. + */ +export class HookIncomings { + readonly #fetch: FetchFunction; + + /** + * Create a new HookIncomings instance. + * @param fetch Function for making API requests + */ + constructor(fetch: FetchFunction) { + this.#fetch = fetch; + } + + /** + * List items currently queued for a webhook. + * @param hookId The hook ID to list queued items for + * @param options Optional filtering and pagination parameters + * @returns Promise with the list of queued items + */ + async list(hookId: number, options?: ListHookIncomingsOptions): Promise { + return ( + await this.#fetch(`/hooks/${hookId}/incomings`, { + query: { + from: options?.from, + to: options?.to, + pg: options?.pg, + }, + }) + ).incomings; + } +} diff --git a/src/endpoints/hooks.ts b/src/endpoints/hooks.ts index a35353f..c29d75f 100644 --- a/src/endpoints/hooks.ts +++ b/src/endpoints/hooks.ts @@ -1,4 +1,5 @@ import type { FetchFunction, JSONValue, Pagination } from '../types.js'; +import { HookIncomings } from './hook-incomings.js'; /** * Represents a Make webhook or mailhook. @@ -147,12 +148,18 @@ export type ListHooksOptions = { export class Hooks { readonly #fetch: FetchFunction; + /** + * Access to a webhook's processing queue (list, stats, detail, delete). + */ + public readonly incomings: HookIncomings; + /** * Create a new Hooks instance. * @param fetch Function for making API requests */ constructor(fetch: FetchFunction) { this.#fetch = fetch; + this.incomings = new HookIncomings(fetch); } /** diff --git a/test/hook-incomings.spec.ts b/test/hook-incomings.spec.ts new file mode 100644 index 0000000..5e5f3d0 --- /dev/null +++ b/test/hook-incomings.spec.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; +import { mockFetch } from './test.utils.js'; + +import * as listMock from './mocks/hook-incomings/list.json'; + +const MAKE_API_KEY = 'api-key'; +const MAKE_ZONE = 'make.local'; +const HOOK_ID = 11; + +describe('Endpoints: HookIncomings', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + it('Should list queued items for a hook', async () => { + mockFetch(`GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings`, listMock); + + const result = await make.hooks.incomings.list(HOOK_ID); + + expect(result).toStrictEqual(listMock.incomings); + }); + + it('Should forward from/to and pagination options when listing', async () => { + mockFetch( + `GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings?from=1000&to=2000&pg%5Blimit%5D=10&pg%5Boffset%5D=0`, + listMock, + ); + + const result = await make.hooks.incomings.list(HOOK_ID, { + from: 1000, + to: 2000, + pg: { limit: 10, offset: 0 }, + }); + + expect(result).toStrictEqual(listMock.incomings); + }); +}); diff --git a/test/mocks/hook-incomings/list.json b/test/mocks/hook-incomings/list.json new file mode 100644 index 0000000..3cc0167 --- /dev/null +++ b/test/mocks/hook-incomings/list.json @@ -0,0 +1,22 @@ +{ + "incomings": [ + { + "id": "a17c1163d7e04d258fce6bac2c8bd3d6", + "scope": "hook", + "size": 1, + "created": "2021-02-03T09:59:36.260Z" + }, + { + "id": "73b56c93f1ff49fe880eeab4fe4c029b", + "scope": "hook", + "size": 1, + "created": "2021-02-03T09:59:38.594Z" + } + ], + "pg": { + "sortBy": "created", + "limit": 10000, + "sortDir": "asc", + "offset": 0 + } +} From 79f823a57a123e3c5d93d5090344ffca4967700c Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 13:40:27 +0200 Subject: [PATCH 02/16] fix: remove unused JSONValue import from hook-incomings.ts --- src/endpoints/hook-incomings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/hook-incomings.ts b/src/endpoints/hook-incomings.ts index e3de4cc..61f4bdd 100644 --- a/src/endpoints/hook-incomings.ts +++ b/src/endpoints/hook-incomings.ts @@ -1,4 +1,4 @@ -import type { FetchFunction, JSONValue, Pagination } from '../types.js'; +import type { FetchFunction, Pagination } from '../types.js'; /** * A single item in a webhook's processing queue. From 027e6e6a41cfb425e882f0ac84c5e5c26873bfab Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 13:43:11 +0200 Subject: [PATCH 03/16] feat: add HookIncomings.stats() --- src/endpoints/hook-incomings.ts | 29 ++++++++++++++++++++++++++++ test/hook-incomings.spec.ts | 9 +++++++++ test/mocks/hook-incomings/stats.json | 7 +++++++ 3 files changed, 45 insertions(+) create mode 100644 test/mocks/hook-incomings/stats.json diff --git a/src/endpoints/hook-incomings.ts b/src/endpoints/hook-incomings.ts index 61f4bdd..18b4bc6 100644 --- a/src/endpoints/hook-incomings.ts +++ b/src/endpoints/hook-incomings.ts @@ -38,6 +38,26 @@ type ListHookIncomingsResponse = { pg: Partial>; }; +/** + * Queue size and limit for a webhook. + */ +export type HookIncomingStats = { + /** Number of items currently in the queue */ + queue: number; + /** Maximum number of items the queue can hold */ + limit: number; + /** Whether the queue is enabled */ + enabled: boolean; +}; + +/** + * Response format for getting a webhook's queue stats. + */ +type GetHookIncomingStatsResponse = { + /** The queue stats */ + incomingStat: HookIncomingStats; +}; + /** * Class providing methods for working with a Make webhook's processing queue. * Items accumulate here when a webhook receives data it can't immediately @@ -71,4 +91,13 @@ export class HookIncomings { }) ).incomings; } + + /** + * Get queue size and limit for a webhook. + * @param hookId The hook ID to get queue stats for + * @returns Promise with the queue stats + */ + async stats(hookId: number): Promise { + return (await this.#fetch(`/hooks/${hookId}/incomings/stats`)).incomingStat; + } } diff --git a/test/hook-incomings.spec.ts b/test/hook-incomings.spec.ts index 5e5f3d0..72d9804 100644 --- a/test/hook-incomings.spec.ts +++ b/test/hook-incomings.spec.ts @@ -3,6 +3,7 @@ import { Make } from '../src/make.js'; import { mockFetch } from './test.utils.js'; import * as listMock from './mocks/hook-incomings/list.json'; +import * as statsMock from './mocks/hook-incomings/stats.json'; const MAKE_API_KEY = 'api-key'; const MAKE_ZONE = 'make.local'; @@ -33,4 +34,12 @@ describe('Endpoints: HookIncomings', () => { expect(result).toStrictEqual(listMock.incomings); }); + + it('Should get queue stats for a hook', async () => { + mockFetch(`GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings/stats`, statsMock); + + const result = await make.hooks.incomings.stats(HOOK_ID); + + expect(result).toStrictEqual(statsMock.incomingStat); + }); }); diff --git a/test/mocks/hook-incomings/stats.json b/test/mocks/hook-incomings/stats.json new file mode 100644 index 0000000..6b675b3 --- /dev/null +++ b/test/mocks/hook-incomings/stats.json @@ -0,0 +1,7 @@ +{ + "incomingStat": { + "queue": 2, + "limit": 10000, + "enabled": true + } +} From 4291fb88e2d6165d86a6bdc4d3306c2fcf4285f5 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 13:47:23 +0200 Subject: [PATCH 04/16] feat: add HookIncomings.get() --- src/endpoints/hook-incomings.ts | 30 ++++++++++++++++++- test/hook-incomings.spec.ts | 25 ++++++++++++++++ .../hook-incomings/get-confidential.json | 9 ++++++ test/mocks/hook-incomings/get.json | 11 +++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 test/mocks/hook-incomings/get-confidential.json create mode 100644 test/mocks/hook-incomings/get.json diff --git a/src/endpoints/hook-incomings.ts b/src/endpoints/hook-incomings.ts index 18b4bc6..a4f37a3 100644 --- a/src/endpoints/hook-incomings.ts +++ b/src/endpoints/hook-incomings.ts @@ -1,4 +1,4 @@ -import type { FetchFunction, Pagination } from '../types.js'; +import type { FetchFunction, JSONValue, Pagination } from '../types.js'; /** * A single item in a webhook's processing queue. @@ -58,6 +58,24 @@ type GetHookIncomingStatsResponse = { incomingStat: HookIncomingStats; }; +/** + * Detail of a single webhook queue item, including its payload. + */ +export type HookIncomingDetail = HookIncoming & { + /** The queued payload. Omitted when the hook is confidential. */ + data?: JSONValue; + /** Whether the hook is confidential, in which case the payload is withheld */ + isHookConfidential?: boolean; +}; + +/** + * Response format for getting a webhook queue item's detail. + */ +type GetHookIncomingResponse = { + /** The requested queue item, including its payload */ + incoming: HookIncomingDetail; +}; + /** * Class providing methods for working with a Make webhook's processing queue. * Items accumulate here when a webhook receives data it can't immediately @@ -100,4 +118,14 @@ export class HookIncomings { async stats(hookId: number): Promise { return (await this.#fetch(`/hooks/${hookId}/incomings/stats`)).incomingStat; } + + /** + * Get detail of a single queued item, including its payload. + * @param hookId The hook ID the queue item belongs to + * @param incomingId The ID of the queue item to retrieve + * @returns Promise with the queue item detail + */ + async get(hookId: number, incomingId: string): Promise { + return (await this.#fetch(`/hooks/${hookId}/incomings/${incomingId}`)).incoming; + } } diff --git a/test/hook-incomings.spec.ts b/test/hook-incomings.spec.ts index 72d9804..4e2ecfa 100644 --- a/test/hook-incomings.spec.ts +++ b/test/hook-incomings.spec.ts @@ -4,6 +4,8 @@ import { mockFetch } from './test.utils.js'; import * as listMock from './mocks/hook-incomings/list.json'; import * as statsMock from './mocks/hook-incomings/stats.json'; +import * as getMock from './mocks/hook-incomings/get.json'; +import * as getConfidentialMock from './mocks/hook-incomings/get-confidential.json'; const MAKE_API_KEY = 'api-key'; const MAKE_ZONE = 'make.local'; @@ -42,4 +44,27 @@ describe('Endpoints: HookIncomings', () => { expect(result).toStrictEqual(statsMock.incomingStat); }); + + it('Should get detail of a queued item', async () => { + mockFetch( + `GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings/8d88f6f5b0484908890ef11fe7e5bf63`, + getMock, + ); + + const result = await make.hooks.incomings.get(HOOK_ID, '8d88f6f5b0484908890ef11fe7e5bf63'); + + expect(result).toStrictEqual(getMock.incoming); + }); + + it('Should omit the payload for a confidential hook', async () => { + mockFetch( + `GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings/7a567f385d1a4f5ab7bff89162b7605e`, + getConfidentialMock, + ); + + const result = await make.hooks.incomings.get(HOOK_ID, '7a567f385d1a4f5ab7bff89162b7605e'); + + expect(result).toStrictEqual(getConfidentialMock.incoming); + expect(result.data).toBeUndefined(); + }); }); diff --git a/test/mocks/hook-incomings/get-confidential.json b/test/mocks/hook-incomings/get-confidential.json new file mode 100644 index 0000000..5b9e8a7 --- /dev/null +++ b/test/mocks/hook-incomings/get-confidential.json @@ -0,0 +1,9 @@ +{ + "incoming": { + "id": "7a567f385d1a4f5ab7bff89162b7605e", + "scope": "hook", + "size": 31, + "created": "2020-03-05T14:48:10.537Z", + "isHookConfidential": true + } +} diff --git a/test/mocks/hook-incomings/get.json b/test/mocks/hook-incomings/get.json new file mode 100644 index 0000000..3ecf3b5 --- /dev/null +++ b/test/mocks/hook-incomings/get.json @@ -0,0 +1,11 @@ +{ + "incoming": { + "id": "8d88f6f5b0484908890ef11fe7e5bf63", + "scope": "hook", + "size": 11, + "created": "2020-03-05T14:52:01.359Z", + "data": { + "name": "test" + } + } +} From 6de238bd61d85f29731946d1d696f2f7a0008b6f Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 13:51:20 +0200 Subject: [PATCH 05/16] feat: add HookIncomings.delete() --- src/endpoints/hook-incomings.ts | 59 +++++++++++++++++++ test/hook-incomings.spec.ts | 39 ++++++++++++ .../hook-incomings/delete-partial-error.json | 7 +++ test/mocks/hook-incomings/delete.json | 3 + 4 files changed, 108 insertions(+) create mode 100644 test/mocks/hook-incomings/delete-partial-error.json create mode 100644 test/mocks/hook-incomings/delete.json diff --git a/src/endpoints/hook-incomings.ts b/src/endpoints/hook-incomings.ts index a4f37a3..8f88d0e 100644 --- a/src/endpoints/hook-incomings.ts +++ b/src/endpoints/hook-incomings.ts @@ -76,6 +76,49 @@ type GetHookIncomingResponse = { incoming: HookIncomingDetail; }; +/** + * Options for deleting items from a webhook's queue. + * Either `ids` or `all` must be specified. + */ +export type DeleteHookIncomingsOptions = { + /** IDs of the queue items to delete */ + ids?: string[]; + /** When used with `all`, IDs of queue items to keep instead of deleting */ + exceptIds?: string[]; + /** Delete every item in the queue */ + all?: boolean; + /** Required (and must be `true`) when `all` is used, to confirm the bulk deletion */ + confirmed?: boolean; +}; + +/** + * Result of deleting items from a webhook's queue. + */ +export type DeleteHookIncomingsResult = { + /** IDs of the queue items that were actually deleted */ + deletedIds: string[]; + /** Present when some items could not be deleted because they were being processed */ + error?: { + /** Name of the error */ + name: string; + /** Description of the error */ + message: string; + }; +}; + +/** + * Response format for deleting items from a webhook's queue. + */ +type DeleteHookIncomingsResponse = { + /** IDs of the queue items that were actually deleted */ + incomings: string[]; + /** Present when some items could not be deleted because they were being processed */ + error?: { + name: string; + message: string; + }; +}; + /** * Class providing methods for working with a Make webhook's processing queue. * Items accumulate here when a webhook receives data it can't immediately @@ -128,4 +171,20 @@ export class HookIncomings { async get(hookId: number, incomingId: string): Promise { return (await this.#fetch(`/hooks/${hookId}/incomings/${incomingId}`)).incoming; } + + /** + * Delete items from a webhook's queue. + * @param hookId The hook ID to delete queue items for + * @param options Which items to delete + * @returns Promise with the IDs that were deleted and an optional partial-failure error + */ + async delete(hookId: number, options: DeleteHookIncomingsOptions): Promise { + const { confirmed, ...body } = options; + const response = await this.#fetch(`/hooks/${hookId}/incomings`, { + method: 'DELETE', + query: { confirmed }, + body, + }); + return { deletedIds: response.incomings, error: response.error }; + } } diff --git a/test/hook-incomings.spec.ts b/test/hook-incomings.spec.ts index 4e2ecfa..3cbd1b4 100644 --- a/test/hook-incomings.spec.ts +++ b/test/hook-incomings.spec.ts @@ -6,6 +6,8 @@ import * as listMock from './mocks/hook-incomings/list.json'; import * as statsMock from './mocks/hook-incomings/stats.json'; import * as getMock from './mocks/hook-incomings/get.json'; import * as getConfidentialMock from './mocks/hook-incomings/get-confidential.json'; +import * as deleteMock from './mocks/hook-incomings/delete.json'; +import * as deletePartialErrorMock from './mocks/hook-incomings/delete-partial-error.json'; const MAKE_API_KEY = 'api-key'; const MAKE_ZONE = 'make.local'; @@ -67,4 +69,41 @@ describe('Endpoints: HookIncomings', () => { expect(result).toStrictEqual(getConfidentialMock.incoming); expect(result.data).toBeUndefined(); }); + + it('Should delete specific queue items', async () => { + const ids = ['d1efa5318a034d36ad7cbeac543573cf', '29d9a7410dff494ab739036f6c332335']; + + mockFetch(`DELETE https://make.local/api/v2/hooks/${HOOK_ID}/incomings`, deleteMock, req => { + expect(req.body).toStrictEqual({ ids }); + }); + + const result = await make.hooks.incomings.delete(HOOK_ID, { ids }); + + expect(result).toStrictEqual({ deletedIds: deleteMock.incomings, error: undefined }); + }); + + it('Should surface a partial-failure error alongside whatever was deleted', async () => { + const ids = ['02731358e5ab4022aff040015a1f1a57', 'dcf18b685e5c4095b9ee24cea09146d3']; + + mockFetch(`DELETE https://make.local/api/v2/hooks/${HOOK_ID}/incomings`, deletePartialErrorMock, req => { + expect(req.body).toStrictEqual({ ids }); + }); + + const result = await make.hooks.incomings.delete(HOOK_ID, { ids }); + + expect(result.deletedIds).toStrictEqual(deletePartialErrorMock.incomings); + expect(result.error).toStrictEqual(deletePartialErrorMock.error); + }); + + it('Should confirm deletion of the entire queue', async () => { + mockFetch( + `DELETE https://make.local/api/v2/hooks/${HOOK_ID}/incomings?confirmed=true`, + deleteMock, + req => { + expect(req.body).toStrictEqual({ all: true }); + }, + ); + + await make.hooks.incomings.delete(HOOK_ID, { all: true, confirmed: true }); + }); }); diff --git a/test/mocks/hook-incomings/delete-partial-error.json b/test/mocks/hook-incomings/delete-partial-error.json new file mode 100644 index 0000000..e9686e6 --- /dev/null +++ b/test/mocks/hook-incomings/delete-partial-error.json @@ -0,0 +1,7 @@ +{ + "incomings": ["02731358e5ab4022aff040015a1f1a57"], + "error": { + "name": "APIError", + "message": "Some of the incoming messages could not be deleted because they are being processed right now." + } +} diff --git a/test/mocks/hook-incomings/delete.json b/test/mocks/hook-incomings/delete.json new file mode 100644 index 0000000..25b1459 --- /dev/null +++ b/test/mocks/hook-incomings/delete.json @@ -0,0 +1,3 @@ +{ + "incomings": ["d1efa5318a034d36ad7cbeac543573cf", "29d9a7410dff494ab739036f6c332335"] +} From eaf78c3d573fada434e3d00c61ab0af36dfc3c9b Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 13:59:03 +0200 Subject: [PATCH 06/16] feat: export HookIncomings public API --- src/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/index.ts b/src/index.ts index eaa815e..8194fa8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -112,6 +112,15 @@ export type { ListFunctionsOptions, } from './endpoints/functions.js'; export type { Hook, Hooks, CreateHookBody, UpdateHookBody, ListHooksOptions, HookPing } from './endpoints/hooks.js'; +export type { + HookIncoming, + HookIncomingDetail, + HookIncomingStats, + HookIncomings, + ListHookIncomingsOptions, + DeleteHookIncomingsOptions, + DeleteHookIncomingsResult, +} from './endpoints/hook-incomings.js'; export type { IncompleteExecution, IncompleteExecutions, From 7e1a1911fdac92c5bb26ef611f9e4cdb917c1345 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 14:03:50 +0200 Subject: [PATCH 07/16] feat: add MCP tools for the webhook incoming queue --- src/endpoints/hook-incomings.tools.ts | 139 ++++++++++++++++++++++++++ src/tools.ts | 2 + test/hook-incomings-tools.spec.ts | 68 +++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 src/endpoints/hook-incomings.tools.ts create mode 100644 test/hook-incomings-tools.spec.ts diff --git a/src/endpoints/hook-incomings.tools.ts b/src/endpoints/hook-incomings.tools.ts new file mode 100644 index 0000000..04a133c --- /dev/null +++ b/src/endpoints/hook-incomings.tools.ts @@ -0,0 +1,139 @@ +import type { Make } from '../make.js'; +import type { MakeTool } from '../tools.js'; + +export const tools: MakeTool[] = [ + { + name: 'hook-incomings_list', + title: 'List webhook queue items', + description: + "List items currently waiting in a webhook's processing queue — payloads the webhook received but hasn't handed off to a scenario yet (e.g. because the scenario isn't active).", + category: 'hook-incomings', + scope: 'hooks:read', + scopeId: 'hookId', + identifier: 'hookId', + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: false, + }, + inputSchema: { + type: 'object', + properties: { + hookId: { type: 'number', description: 'The hook ID to list queued items for' }, + from: { + type: 'number', + description: 'Only include items queued at or after this Unix timestamp (ms)', + }, + to: { + type: 'number', + description: 'Only include items queued at or before this Unix timestamp (ms)', + }, + }, + required: ['hookId'], + }, + examples: [{ hookId: 11 }], + execute: async (make: Make, args: { hookId: number; from?: number; to?: number }) => { + return await make.hooks.incomings.list(args.hookId, { from: args.from, to: args.to }); + }, + }, + { + name: 'hook-incomings_stats', + title: 'Get webhook queue stats', + description: "Get the current size and limit of a webhook's processing queue.", + category: 'hook-incomings', + scope: 'hooks:read', + scopeId: 'hookId', + identifier: 'hookId', + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: false, + }, + inputSchema: { + type: 'object', + properties: { + hookId: { type: 'number', description: 'The hook ID to get queue stats for' }, + }, + required: ['hookId'], + }, + examples: [{ hookId: 11 }], + execute: async (make: Make, args: { hookId: number }) => { + return await make.hooks.incomings.stats(args.hookId); + }, + }, + { + name: 'hook-incomings_get', + title: 'Get webhook queue item detail', + description: 'Get the full detail of a single queued item, including its payload.', + category: 'hook-incomings', + scope: 'hooks:read', + scopeId: 'hookId', + identifier: 'hookId', + resourceId: 'incomingId', + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: false, + }, + inputSchema: { + type: 'object', + properties: { + hookId: { type: 'number', description: 'The hook ID the queue item belongs to' }, + incomingId: { type: 'string', description: 'The ID of the queue item to retrieve' }, + }, + required: ['hookId', 'incomingId'], + }, + examples: [{ hookId: 11, incomingId: '7a567f385d1a4f5ab7bff89162b7605e' }], + execute: async (make: Make, args: { hookId: number; incomingId: string }) => { + return await make.hooks.incomings.get(args.hookId, args.incomingId); + }, + }, + { + name: 'hook-incomings_delete', + title: 'Delete webhook queue items', + description: + "Delete items from a webhook's processing queue. Specify `ids` to delete specific items, or `all: true` (with `confirmed: true`) to clear the entire queue. An item currently being processed cannot be deleted.", + category: 'hook-incomings', + scope: 'hooks:write', + scopeId: 'hookId', + identifier: 'hookId', + annotations: { + readOnlyHint: false, + destructiveHint: true, + openWorldHint: false, + }, + inputSchema: { + type: 'object', + properties: { + hookId: { type: 'number', description: 'The hook ID to delete queue items for' }, + ids: { + type: 'array', + items: { type: 'string' }, + description: 'IDs of the queue items to delete', + }, + exceptIds: { + type: 'array', + items: { type: 'string' }, + description: 'When used with `all`, IDs of queue items to keep instead of deleting', + }, + all: { type: 'boolean', description: 'Delete every item in the queue' }, + confirmed: { + type: 'boolean', + description: 'Required (and must be `true`) when `all` is used, to confirm the bulk deletion', + }, + }, + required: ['hookId'], + }, + examples: [ + { hookId: 11, ids: ['d1efa5318a034d36ad7cbeac543573cf'] }, + { hookId: 11, all: true, confirmed: true }, + ], + execute: async ( + make: Make, + args: { hookId: number; ids?: string[]; exceptIds?: string[]; all?: boolean; confirmed?: boolean }, + ) => { + const { hookId, ...options } = args; + return await make.hooks.incomings.delete(hookId, options); + }, + }, +]; diff --git a/src/tools.ts b/src/tools.ts index 4ff05d3..b6872c1 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -20,6 +20,7 @@ import { tools as UsersTools } from './endpoints/users.tools.js'; import { tools as FunctionsTools } from './endpoints/functions.tools.js'; import { tools as ExecutionsTools } from './endpoints/executions.tools.js'; import { tools as HooksTools } from './endpoints/hooks.tools.js'; +import { tools as HookIncomingsTools } from './endpoints/hook-incomings.tools.js'; import { tools as DevicesTools } from './endpoints/devices.tools.js'; import { tools as KeysTools } from './endpoints/keys.tools.js'; import { tools as FoldersTools } from './endpoints/folders.tools.js'; @@ -225,6 +226,7 @@ export const MakeTools = [ ...ScenarioLabelsTools, ...FunctionsTools, ...HooksTools, + ...HookIncomingsTools, ...DevicesTools, ...DataStructuresTools, ...ConnectionsTools, diff --git a/test/hook-incomings-tools.spec.ts b/test/hook-incomings-tools.spec.ts new file mode 100644 index 0000000..cfb743c --- /dev/null +++ b/test/hook-incomings-tools.spec.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; +import { MakeTools } from '../src/tools.js'; +import { mockFetch } from './test.utils.js'; + +import * as listMock from './mocks/hook-incomings/list.json'; +import * as statsMock from './mocks/hook-incomings/stats.json'; +import * as getMock from './mocks/hook-incomings/get.json'; +import * as deleteMock from './mocks/hook-incomings/delete.json'; + +const MAKE_API_KEY = 'api-key'; +const MAKE_ZONE = 'make.local'; +const HOOK_ID = 11; + +function getTool(name: string) { + const tool = MakeTools.find(entry => entry.name === name); + if (!tool) { + throw new Error(`Missing MCP tool: ${name}`); + } + return tool; +} + +describe('MCP tools: hook-incomings', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + it('Should execute hook-incomings_list', async () => { + mockFetch(`GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings`, listMock); + + const tool = getTool('hook-incomings_list'); + const result = await tool.execute(make, { hookId: HOOK_ID }); + + expect(result).toStrictEqual(listMock.incomings); + }); + + it('Should execute hook-incomings_stats', async () => { + mockFetch(`GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings/stats`, statsMock); + + const tool = getTool('hook-incomings_stats'); + const result = await tool.execute(make, { hookId: HOOK_ID }); + + expect(result).toStrictEqual(statsMock.incomingStat); + }); + + it('Should execute hook-incomings_get', async () => { + mockFetch( + `GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings/8d88f6f5b0484908890ef11fe7e5bf63`, + getMock, + ); + + const tool = getTool('hook-incomings_get'); + const result = await tool.execute(make, { hookId: HOOK_ID, incomingId: '8d88f6f5b0484908890ef11fe7e5bf63' }); + + expect(result).toStrictEqual(getMock.incoming); + }); + + it('Should execute hook-incomings_delete', async () => { + const ids = ['d1efa5318a034d36ad7cbeac543573cf', '29d9a7410dff494ab739036f6c332335']; + + mockFetch(`DELETE https://make.local/api/v2/hooks/${HOOK_ID}/incomings`, deleteMock, req => { + expect(req.body).toStrictEqual({ ids }); + }); + + const tool = getTool('hook-incomings_delete'); + const result = await tool.execute(make, { hookId: HOOK_ID, ids }); + + expect(result).toStrictEqual({ deletedIds: deleteMock.incomings, error: undefined }); + }); +}); From 05079d98134615f4eadf49c0aac56a8030603a06 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 14:13:59 +0200 Subject: [PATCH 08/16] docs: document the webhook incoming queue endpoints --- README.md | 2 ++ test/hook-incomings-tools.spec.ts | 5 +---- test/hook-incomings.spec.ts | 15 ++++----------- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1b79918..bc0d89d 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ const make = new Make('your-api-key', 'eu2.make.com', { - **Scenario Labels** - Team-scoped tags assignable to scenarios - **Functions** - Custom JavaScript functions for scenarios - **Hooks** - Webhooks and mailhooks for external integrations +- **Hook Incoming Queue** - Inspect and clear items queued for a webhook awaiting processing - **Incomplete Executions** - Failed or incomplete scenario runs - **Keys** - API keys and secrets - **On-Prem Agents** - On-prem bridge agents running on customer infrastructure @@ -212,6 +213,7 @@ All tools are organized into the following categories: - `folders` - `functions` - `hooks` +- `hook-incomings` - `incomplete-executions` - `keys` - `scenario-labels` diff --git a/test/hook-incomings-tools.spec.ts b/test/hook-incomings-tools.spec.ts index cfb743c..9b4c693 100644 --- a/test/hook-incomings-tools.spec.ts +++ b/test/hook-incomings-tools.spec.ts @@ -42,10 +42,7 @@ describe('MCP tools: hook-incomings', () => { }); it('Should execute hook-incomings_get', async () => { - mockFetch( - `GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings/8d88f6f5b0484908890ef11fe7e5bf63`, - getMock, - ); + mockFetch(`GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings/8d88f6f5b0484908890ef11fe7e5bf63`, getMock); const tool = getTool('hook-incomings_get'); const result = await tool.execute(make, { hookId: HOOK_ID, incomingId: '8d88f6f5b0484908890ef11fe7e5bf63' }); diff --git a/test/hook-incomings.spec.ts b/test/hook-incomings.spec.ts index 3cbd1b4..9f411f1 100644 --- a/test/hook-incomings.spec.ts +++ b/test/hook-incomings.spec.ts @@ -48,10 +48,7 @@ describe('Endpoints: HookIncomings', () => { }); it('Should get detail of a queued item', async () => { - mockFetch( - `GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings/8d88f6f5b0484908890ef11fe7e5bf63`, - getMock, - ); + mockFetch(`GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings/8d88f6f5b0484908890ef11fe7e5bf63`, getMock); const result = await make.hooks.incomings.get(HOOK_ID, '8d88f6f5b0484908890ef11fe7e5bf63'); @@ -96,13 +93,9 @@ describe('Endpoints: HookIncomings', () => { }); it('Should confirm deletion of the entire queue', async () => { - mockFetch( - `DELETE https://make.local/api/v2/hooks/${HOOK_ID}/incomings?confirmed=true`, - deleteMock, - req => { - expect(req.body).toStrictEqual({ all: true }); - }, - ); + mockFetch(`DELETE https://make.local/api/v2/hooks/${HOOK_ID}/incomings?confirmed=true`, deleteMock, req => { + expect(req.body).toStrictEqual({ all: true }); + }); await make.hooks.incomings.delete(HOOK_ID, { all: true, confirmed: true }); }); From 6c0100143eb683d74eeada1d31a02afb67f89fdd Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 15:10:19 +0200 Subject: [PATCH 09/16] chore: bump version to 1.6.14 Co-Authored-By: Claude Sonnet 5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50eb6b1..bc0e78f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@makehq/sdk", - "version": "1.6.13", + "version": "1.6.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@makehq/sdk", - "version": "1.6.13", + "version": "1.6.14", "license": "MIT", "devDependencies": { "@eslint/js": "^9.22.0", diff --git a/package.json b/package.json index 7770eb1..9361bae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makehq/sdk", - "version": "1.6.13", + "version": "1.6.14", "description": "Make TypeScript SDK", "license": "MIT", "author": "Make", From 6375347acd84d3619445f3938c6b35ee91a22687 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 16:23:59 +0200 Subject: [PATCH 10/16] fix(hook-incomings): enforce delete selectors - #discussion_r3904131817 validate SDK delete options before sending a request\n- #discussion_r3904131897 encode the two valid delete shapes in the tool schema --- src/endpoints/hook-incomings.tools.ts | 28 +++++++++++++++--- src/endpoints/hook-incomings.ts | 41 ++++++++++++++++++++------- test/hook-incomings-tools.spec.ts | 17 +++++++++++ test/hook-incomings.spec.ts | 13 +++++++++ 4 files changed, 85 insertions(+), 14 deletions(-) diff --git a/src/endpoints/hook-incomings.tools.ts b/src/endpoints/hook-incomings.tools.ts index 04a133c..93c232c 100644 --- a/src/endpoints/hook-incomings.tools.ts +++ b/src/endpoints/hook-incomings.tools.ts @@ -1,5 +1,6 @@ import type { Make } from '../make.js'; import type { MakeTool } from '../tools.js'; +import type { DeleteHookIncomingsOptions } from './hook-incomings.js'; export const tools: MakeTool[] = [ { @@ -123,15 +124,34 @@ export const tools: MakeTool[] = [ }, }, required: ['hookId'], + oneOf: [ + { + type: 'object', + properties: { + hookId: { type: 'number' }, + ids: { type: 'array', items: { type: 'string' } }, + }, + required: ['ids'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + hookId: { type: 'number' }, + exceptIds: { type: 'array', items: { type: 'string' } }, + all: { type: 'boolean', const: true }, + confirmed: { type: 'boolean', const: true }, + }, + required: ['all', 'confirmed'], + additionalProperties: false, + }, + ], }, examples: [ { hookId: 11, ids: ['d1efa5318a034d36ad7cbeac543573cf'] }, { hookId: 11, all: true, confirmed: true }, ], - execute: async ( - make: Make, - args: { hookId: number; ids?: string[]; exceptIds?: string[]; all?: boolean; confirmed?: boolean }, - ) => { + execute: async (make: Make, args: { hookId: number } & DeleteHookIncomingsOptions) => { const { hookId, ...options } = args; return await make.hooks.incomings.delete(hookId, options); }, diff --git a/src/endpoints/hook-incomings.ts b/src/endpoints/hook-incomings.ts index 8f88d0e..d14b81a 100644 --- a/src/endpoints/hook-incomings.ts +++ b/src/endpoints/hook-incomings.ts @@ -80,16 +80,27 @@ type GetHookIncomingResponse = { * Options for deleting items from a webhook's queue. * Either `ids` or `all` must be specified. */ -export type DeleteHookIncomingsOptions = { - /** IDs of the queue items to delete */ - ids?: string[]; - /** When used with `all`, IDs of queue items to keep instead of deleting */ - exceptIds?: string[]; - /** Delete every item in the queue */ - all?: boolean; - /** Required (and must be `true`) when `all` is used, to confirm the bulk deletion */ - confirmed?: boolean; -}; +export type DeleteHookIncomingsOptions = + | { + /** IDs of the queue items to delete */ + ids: string[]; + /** Not available when deleting specific queue items */ + exceptIds?: never; + /** Not available when deleting specific queue items */ + all?: never; + /** Not available when deleting specific queue items */ + confirmed?: never; + } + | { + /** Not available when deleting the entire queue */ + ids?: never; + /** IDs of queue items to keep instead of deleting */ + exceptIds?: string[]; + /** Delete every item in the queue */ + all: true; + /** Confirm the bulk deletion */ + confirmed: true; + }; /** * Result of deleting items from a webhook's queue. @@ -179,6 +190,16 @@ export class HookIncomings { * @returns Promise with the IDs that were deleted and an optional partial-failure error */ async delete(hookId: number, options: DeleteHookIncomingsOptions): Promise { + const deletesSpecificItems = options.ids !== undefined; + const deletesAllItems = options.all === true; + + if (deletesSpecificItems === deletesAllItems) { + throw new TypeError('Exactly one of `ids` or `all: true` must be specified'); + } + if (deletesAllItems && options.confirmed !== true) { + throw new TypeError('`confirmed` must be `true` when `all` is used'); + } + const { confirmed, ...body } = options; const response = await this.#fetch(`/hooks/${hookId}/incomings`, { method: 'DELETE', diff --git a/test/hook-incomings-tools.spec.ts b/test/hook-incomings-tools.spec.ts index 9b4c693..8cead1a 100644 --- a/test/hook-incomings-tools.spec.ts +++ b/test/hook-incomings-tools.spec.ts @@ -62,4 +62,21 @@ describe('MCP tools: hook-incomings', () => { expect(result).toStrictEqual({ deletedIds: deleteMock.incomings, error: undefined }); }); + + it('Should describe the valid hook-incomings_delete input variants', () => { + const tool = getTool('hook-incomings_delete'); + + expect(tool.inputSchema.oneOf).toEqual( + expect.arrayContaining([ + expect.objectContaining({ required: ['ids'] }), + expect.objectContaining({ + required: ['all', 'confirmed'], + properties: expect.objectContaining({ + all: expect.objectContaining({ const: true }), + confirmed: expect.objectContaining({ const: true }), + }), + }), + ]), + ); + }); }); diff --git a/test/hook-incomings.spec.ts b/test/hook-incomings.spec.ts index 9f411f1..1ee79ea 100644 --- a/test/hook-incomings.spec.ts +++ b/test/hook-incomings.spec.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from '@jest/globals'; import { Make } from '../src/make.js'; +import type { DeleteHookIncomingsOptions } from '../src/endpoints/hook-incomings.js'; import { mockFetch } from './test.utils.js'; import * as listMock from './mocks/hook-incomings/list.json'; @@ -99,4 +100,16 @@ describe('Endpoints: HookIncomings', () => { await make.hooks.incomings.delete(HOOK_ID, { all: true, confirmed: true }); }); + + it('Should reject deletion without a selector', async () => { + await expect(make.hooks.incomings.delete(HOOK_ID, {} as DeleteHookIncomingsOptions)).rejects.toThrow( + 'Exactly one of `ids` or `all: true` must be specified', + ); + }); + + it('Should require explicit confirmation when deleting the entire queue', async () => { + await expect(make.hooks.incomings.delete(HOOK_ID, { all: true } as DeleteHookIncomingsOptions)).rejects.toThrow( + '`confirmed` must be `true` when `all` is used', + ); + }); }); From 17be98127bf99ec97fee23a16a9a361fd86746f1 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 16:50:02 +0200 Subject: [PATCH 11/16] fix(hook-incomings): address self-review findings - self-review hook-incomings-get-unvalidated-path-id: validate and encode queue item path IDs\n- self-review hook-incomings-delete-empty-ids: reject empty selector arrays in the endpoint and tool schema --- src/endpoints/hook-incomings.tools.ts | 9 +++++++-- src/endpoints/hook-incomings.ts | 16 ++++++++++++++-- test/hook-incomings-tools.spec.ts | 9 +++++++++ test/hook-incomings.spec.ts | 12 ++++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/endpoints/hook-incomings.tools.ts b/src/endpoints/hook-incomings.tools.ts index 93c232c..7b30561 100644 --- a/src/endpoints/hook-incomings.tools.ts +++ b/src/endpoints/hook-incomings.tools.ts @@ -80,7 +80,11 @@ export const tools: MakeTool[] = [ type: 'object', properties: { hookId: { type: 'number', description: 'The hook ID the queue item belongs to' }, - incomingId: { type: 'string', description: 'The ID of the queue item to retrieve' }, + incomingId: { + type: 'string', + pattern: '^[0-9a-f]{32}$', + description: 'The 32-character lowercase hexadecimal ID of the queue item to retrieve', + }, }, required: ['hookId', 'incomingId'], }, @@ -110,6 +114,7 @@ export const tools: MakeTool[] = [ ids: { type: 'array', items: { type: 'string' }, + minItems: 1, description: 'IDs of the queue items to delete', }, exceptIds: { @@ -129,7 +134,7 @@ export const tools: MakeTool[] = [ type: 'object', properties: { hookId: { type: 'number' }, - ids: { type: 'array', items: { type: 'string' } }, + ids: { type: 'array', items: { type: 'string' }, minItems: 1 }, }, required: ['ids'], additionalProperties: false, diff --git a/src/endpoints/hook-incomings.ts b/src/endpoints/hook-incomings.ts index d14b81a..ef61916 100644 --- a/src/endpoints/hook-incomings.ts +++ b/src/endpoints/hook-incomings.ts @@ -1,5 +1,7 @@ import type { FetchFunction, JSONValue, Pagination } from '../types.js'; +const HOOK_INCOMING_ID_PATTERN = /^[0-9a-f]{32}$/; + /** * A single item in a webhook's processing queue. * Queue items accumulate when a webhook receives data it can't immediately @@ -176,11 +178,17 @@ export class HookIncomings { /** * Get detail of a single queued item, including its payload. * @param hookId The hook ID the queue item belongs to - * @param incomingId The ID of the queue item to retrieve + * @param incomingId The 32-character lowercase hexadecimal ID of the queue item to retrieve * @returns Promise with the queue item detail + * @throws {TypeError} If `incomingId` is not a valid queue item ID */ async get(hookId: number, incomingId: string): Promise { - return (await this.#fetch(`/hooks/${hookId}/incomings/${incomingId}`)).incoming; + if (!HOOK_INCOMING_ID_PATTERN.test(incomingId)) { + throw new TypeError('`incomingId` must be a 32-character lowercase hexadecimal string'); + } + + const encodedIncomingId = encodeURIComponent(incomingId); + return (await this.#fetch(`/hooks/${hookId}/incomings/${encodedIncomingId}`)).incoming; } /** @@ -188,6 +196,7 @@ export class HookIncomings { * @param hookId The hook ID to delete queue items for * @param options Which items to delete * @returns Promise with the IDs that were deleted and an optional partial-failure error + * @throws {TypeError} If the deletion selector is invalid or a bulk deletion is not confirmed */ async delete(hookId: number, options: DeleteHookIncomingsOptions): Promise { const deletesSpecificItems = options.ids !== undefined; @@ -196,6 +205,9 @@ export class HookIncomings { if (deletesSpecificItems === deletesAllItems) { throw new TypeError('Exactly one of `ids` or `all: true` must be specified'); } + if (deletesSpecificItems && options.ids.length === 0) { + throw new TypeError('`ids` must contain at least one queue item ID'); + } if (deletesAllItems && options.confirmed !== true) { throw new TypeError('`confirmed` must be `true` when `all` is used'); } diff --git a/test/hook-incomings-tools.spec.ts b/test/hook-incomings-tools.spec.ts index 8cead1a..40ec160 100644 --- a/test/hook-incomings-tools.spec.ts +++ b/test/hook-incomings-tools.spec.ts @@ -50,6 +50,12 @@ describe('MCP tools: hook-incomings', () => { expect(result).toStrictEqual(getMock.incoming); }); + it('Should constrain hook-incomings_get to queue item IDs', () => { + const tool = getTool('hook-incomings_get'); + + expect(tool.inputSchema.properties?.incomingId?.pattern).toBe('^[0-9a-f]{32}$'); + }); + it('Should execute hook-incomings_delete', async () => { const ids = ['d1efa5318a034d36ad7cbeac543573cf', '29d9a7410dff494ab739036f6c332335']; @@ -65,6 +71,7 @@ describe('MCP tools: hook-incomings', () => { it('Should describe the valid hook-incomings_delete input variants', () => { const tool = getTool('hook-incomings_delete'); + const idsVariant = tool.inputSchema.oneOf?.find(schema => schema.required?.includes('ids')); expect(tool.inputSchema.oneOf).toEqual( expect.arrayContaining([ @@ -78,5 +85,7 @@ describe('MCP tools: hook-incomings', () => { }), ]), ); + expect(tool.inputSchema.properties?.ids?.minItems).toBe(1); + expect(idsVariant?.properties?.ids?.minItems).toBe(1); }); }); diff --git a/test/hook-incomings.spec.ts b/test/hook-incomings.spec.ts index 1ee79ea..0a48ad7 100644 --- a/test/hook-incomings.spec.ts +++ b/test/hook-incomings.spec.ts @@ -56,6 +56,12 @@ describe('Endpoints: HookIncomings', () => { expect(result).toStrictEqual(getMock.incoming); }); + it.each(['stats', '../../../users/me'])('Should reject malformed queue item ID %s', async incomingId => { + await expect(make.hooks.incomings.get(HOOK_ID, incomingId)).rejects.toThrow( + '`incomingId` must be a 32-character lowercase hexadecimal string', + ); + }); + it('Should omit the payload for a confidential hook', async () => { mockFetch( `GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings/7a567f385d1a4f5ab7bff89162b7605e`, @@ -107,6 +113,12 @@ describe('Endpoints: HookIncomings', () => { ); }); + it('Should reject an empty list of deletion IDs', async () => { + await expect(make.hooks.incomings.delete(HOOK_ID, { ids: [] })).rejects.toThrow( + '`ids` must contain at least one queue item ID', + ); + }); + it('Should require explicit confirmation when deleting the entire queue', async () => { await expect(make.hooks.incomings.delete(HOOK_ID, { all: true } as DeleteHookIncomingsOptions)).rejects.toThrow( '`confirmed` must be `true` when `all` is used', From aa92fe70e1a02672782b9e07f7fe5536ae6717cf Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 17:11:01 +0200 Subject: [PATCH 12/16] fix(hook-incomings): resolve round-2 Copilot feedback - #discussion_r3905273756 add safe live coverage for all queue operations and deletion contracts\n- #discussion_r3905273847 document confidential payload omission in the tool\n- #discussion_r3905273904 align get method documentation with confidential responses --- .env.example | 5 + README.md | 11 ++ src/endpoints/hook-incomings.tools.ts | 3 +- src/endpoints/hook-incomings.ts | 2 +- test/hook-incomings-tools.spec.ts | 6 + test/hook-incomings.integration.test.ts | 166 ++++++++++++++++++++++++ 6 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 test/hook-incomings.integration.test.ts diff --git a/.env.example b/.env.example index 80c7407..b90aba9 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,11 @@ MAKE_ORGANIZATION="12345" # Numeric team ID (used by most integration tests, e.g. scenarios, connections, executions) MAKE_TEAM="12345" +# Dedicated non-confidential scheduled webhook whose inactive scenario leaves deliveries queued; +# token needs hooks:read + hooks:write +MAKE_HOOK_INCOMINGS_HOOK_ID="" +MAKE_HOOK_INCOMINGS_WEBHOOK_URL="" + # Existing SDK app name for SDK integration tests; if unset, a temporary app is created and deleted MAKE_APP_NAME="" diff --git a/README.md b/README.md index bc0d89d..fb74296 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,17 @@ MAKE_TEAM="" MAKE_ORGANIZATION="" ``` +Required only for Hook Incoming Queue integration tests: + +``` +MAKE_HOOK_INCOMINGS_HOOK_ID="" +MAKE_HOOK_INCOMINGS_WEBHOOK_URL="" +``` + +Use a dedicated non-confidential scheduled webhook whose scenario is inactive so deliveries remain queued. The API +token needs `hooks:read` and `hooks:write`; the tests preserve pre-existing queue items and never create or delete the +hook. + Required for connected-system **create** integration (field names from `getAppConfig`): ``` diff --git a/src/endpoints/hook-incomings.tools.ts b/src/endpoints/hook-incomings.tools.ts index 7b30561..3e7e923 100644 --- a/src/endpoints/hook-incomings.tools.ts +++ b/src/endpoints/hook-incomings.tools.ts @@ -65,7 +65,8 @@ export const tools: MakeTool[] = [ { name: 'hook-incomings_get', title: 'Get webhook queue item detail', - description: 'Get the full detail of a single queued item, including its payload.', + description: + 'Get the full detail of a single queued item, including its payload when available. Confidential hooks omit payload data.', category: 'hook-incomings', scope: 'hooks:read', scopeId: 'hookId', diff --git a/src/endpoints/hook-incomings.ts b/src/endpoints/hook-incomings.ts index ef61916..7d7e941 100644 --- a/src/endpoints/hook-incomings.ts +++ b/src/endpoints/hook-incomings.ts @@ -176,7 +176,7 @@ export class HookIncomings { } /** - * Get detail of a single queued item, including its payload. + * Get detail of a single queued item, including its payload when available. Confidential hooks omit payload data. * @param hookId The hook ID the queue item belongs to * @param incomingId The 32-character lowercase hexadecimal ID of the queue item to retrieve * @returns Promise with the queue item detail diff --git a/test/hook-incomings-tools.spec.ts b/test/hook-incomings-tools.spec.ts index 40ec160..62e4ece 100644 --- a/test/hook-incomings-tools.spec.ts +++ b/test/hook-incomings-tools.spec.ts @@ -56,6 +56,12 @@ describe('MCP tools: hook-incomings', () => { expect(tool.inputSchema.properties?.incomingId?.pattern).toBe('^[0-9a-f]{32}$'); }); + it('Should explain that confidential hooks omit queued payload data', () => { + const tool = getTool('hook-incomings_get'); + + expect(tool.description).toContain('Confidential hooks omit payload data'); + }); + it('Should execute hook-incomings_delete', async () => { const ids = ['d1efa5318a034d36ad7cbeac543573cf', '29d9a7410dff494ab739036f6c332335']; diff --git a/test/hook-incomings.integration.test.ts b/test/hook-incomings.integration.test.ts new file mode 100644 index 0000000..d7091ed --- /dev/null +++ b/test/hook-incomings.integration.test.ts @@ -0,0 +1,166 @@ +import 'dotenv/config'; +import { randomUUID } from 'node:crypto'; +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; +import type { HookIncoming, HookIncomingDetail } from '../src/endpoints/hook-incomings.js'; + +const MAKE_API_KEY = String(process.env.MAKE_API_KEY || ''); +const MAKE_ZONE = String(process.env.MAKE_ZONE || ''); +const MAKE_HOOK_INCOMINGS_HOOK_ID = Number(process.env.MAKE_HOOK_INCOMINGS_HOOK_ID || 0); +const MAKE_HOOK_INCOMINGS_WEBHOOK_URL = String(process.env.MAKE_HOOK_INCOMINGS_WEBHOOK_URL || ''); + +const POLL_INTERVAL_MS = 1000; +const POLL_TIMEOUT_MS = 30_000; +const LIST_OPTIONS = { pg: { limit: 10_000, offset: 0 } } as const; +const MARKER_FIELD = 'makeSdkHookIncomingsTestMarker'; +const TEST_MARKERS = [`${randomUUID()}-specific`, `${randomUUID()}-bulk`] as const; +const TEST_MARKER_SET: ReadonlySet = new Set(TEST_MARKERS); + +const requirements = [ + ['MAKE_API_KEY', Boolean(MAKE_API_KEY)], + ['MAKE_ZONE', Boolean(MAKE_ZONE)], + ['MAKE_HOOK_INCOMINGS_HOOK_ID', Number.isInteger(MAKE_HOOK_INCOMINGS_HOOK_ID) && MAKE_HOOK_INCOMINGS_HOOK_ID > 0], + ['MAKE_HOOK_INCOMINGS_WEBHOOK_URL', Boolean(MAKE_HOOK_INCOMINGS_WEBHOOK_URL)], +] as const; +const missingRequirements = requirements.filter(([, ready]) => !ready).map(([name]) => name); +const integrationReady = missingRequirements.length === 0; +const skipHint = integrationReady ? '' : ` — skipped; set ${missingRequirements.join(', ')} in .env`; + +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function payloadMarker(data: HookIncomingDetail['data']): string | undefined { + if (typeof data !== 'object' || data === null || Array.isArray(data)) return undefined; + + const marker = data[MARKER_FIELD]; + return typeof marker === 'string' ? marker : undefined; +} + +(integrationReady ? describe : describe.skip)(`Integration: Hook incomings${skipHint}`, () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + let baselineIds = new Set(); + let testIdsByMarker = new Map(); + + async function listAll(): Promise { + return await make.hooks.incomings.list(MAKE_HOOK_INCOMINGS_HOOK_ID, LIST_OPTIONS); + } + + async function findTestIds(): Promise> { + const incomings = await listAll(); + const candidates = incomings.filter(incoming => !baselineIds.has(incoming.id)); + const found = new Map(); + + for (const candidate of candidates) { + const detail = await make.hooks.incomings.get(MAKE_HOOK_INCOMINGS_HOOK_ID, candidate.id); + const marker = payloadMarker(detail.data); + + if (marker !== undefined && TEST_MARKER_SET.has(marker)) { + found.set(marker, candidate.id); + } + } + + return found; + } + + async function waitForTestIds(): Promise> { + const deadline = Date.now() + POLL_TIMEOUT_MS; + + while (Date.now() < deadline) { + const found = await findTestIds(); + if (found.size === TEST_MARKERS.length) return found; + await sleep(POLL_INTERVAL_MS); + } + + throw new Error(`Timed out waiting for ${TEST_MARKERS.length} marked webhook deliveries to enter the queue`); + } + + function requireTestId(marker: (typeof TEST_MARKERS)[number]): string { + const id = testIdsByMarker.get(marker); + if (!id) throw new Error(`Missing queued webhook delivery for marker ${marker}`); + return id; + } + + beforeAll(async () => { + baselineIds = new Set((await listAll()).map(incoming => incoming.id)); + + for (const marker of TEST_MARKERS) { + const response = await fetch(MAKE_HOOK_INCOMINGS_WEBHOOK_URL, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ [MARKER_FIELD]: marker }), + }); + + if (!response.ok) { + throw new Error(`Webhook fixture rejected a test delivery with HTTP ${response.status}`); + } + } + + testIdsByMarker = await waitForTestIds(); + }, POLL_TIMEOUT_MS + 10_000); + + afterAll(async () => { + const current = await listAll(); + const currentIds = new Set(current.map(incoming => incoming.id)); + const discovered = await findTestIds(); + const remainingTestIds = new Set( + [...testIdsByMarker.values(), ...discovered.values()].filter(id => currentIds.has(id)), + ); + + if (remainingTestIds.size > 0) { + await make.hooks.incomings.delete(MAKE_HOOK_INCOMINGS_HOOK_ID, { + ids: [...remainingTestIds], + }); + } + }, POLL_TIMEOUT_MS); + + it('Should list the two queued fixture deliveries', async () => { + const incomings = await listAll(); + + expect(incomings.map(incoming => incoming.id)).toEqual(expect.arrayContaining([...testIdsByMarker.values()])); + }); + + it('Should report queue stats including the fixture deliveries', async () => { + const stats = await make.hooks.incomings.stats(MAKE_HOOK_INCOMINGS_HOOK_ID); + + expect(stats.enabled).toBe(true); + expect(stats.queue).toBeGreaterThanOrEqual(baselineIds.size + TEST_MARKERS.length); + expect(stats.limit).toBeGreaterThanOrEqual(stats.queue); + }); + + it('Should get each fixture delivery with its payload data', async () => { + const details = await Promise.all( + TEST_MARKERS.map(marker => make.hooks.incomings.get(MAKE_HOOK_INCOMINGS_HOOK_ID, requireTestId(marker))), + ); + + expect(details.map(detail => payloadMarker(detail.data))).toStrictEqual(TEST_MARKERS); + for (const detail of details) { + expect(detail.isHookConfidential).not.toBe(true); + } + }); + + it('Should delete a specific fixture delivery using IDs in the request body', async () => { + const id = requireTestId(TEST_MARKERS[0]); + const result = await make.hooks.incomings.delete(MAKE_HOOK_INCOMINGS_HOOK_ID, { ids: [id] }); + const remainingIds = new Set((await listAll()).map(incoming => incoming.id)); + + expect(result.deletedIds).toContain(id); + expect(remainingIds.has(id)).toBe(false); + expect([...baselineIds].every(baselineId => remainingIds.has(baselineId))).toBe(true); + }); + + it('Should bulk-delete the other fixture delivery with confirmation while preserving the baseline', async () => { + const id = requireTestId(TEST_MARKERS[1]); + const result = await make.hooks.incomings.delete(MAKE_HOOK_INCOMINGS_HOOK_ID, { + all: true, + confirmed: true, + exceptIds: [...baselineIds], + }); + const remainingIds = new Set((await listAll()).map(incoming => incoming.id)); + + expect(result.deletedIds).toContain(id); + expect(remainingIds.has(id)).toBe(false); + expect([...baselineIds].every(baselineId => remainingIds.has(baselineId))).toBe(true); + }); +}); From c1fe5aa524a8ca4b825e1dfac787fea40c3f6617 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 17:16:08 +0200 Subject: [PATCH 13/16] fix(hook-incomings): expose tool pagination - #discussion_r3905374186 add a structured pg schema and forward list pagination options --- src/endpoints/hook-incomings.tools.ts | 32 ++++++++++++++++++++++++--- test/hook-incomings-tools.spec.ts | 24 ++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/endpoints/hook-incomings.tools.ts b/src/endpoints/hook-incomings.tools.ts index 3e7e923..7dcb0f7 100644 --- a/src/endpoints/hook-incomings.tools.ts +++ b/src/endpoints/hook-incomings.tools.ts @@ -1,6 +1,6 @@ import type { Make } from '../make.js'; import type { MakeTool } from '../tools.js'; -import type { DeleteHookIncomingsOptions } from './hook-incomings.js'; +import type { DeleteHookIncomingsOptions, ListHookIncomingsOptions } from './hook-incomings.js'; export const tools: MakeTool[] = [ { @@ -29,12 +29,38 @@ export const tools: MakeTool[] = [ type: 'number', description: 'Only include items queued at or before this Unix timestamp (ms)', }, + pg: { + type: 'object', + description: 'Pagination and sorting options', + properties: { + sortBy: { + type: 'string', + enum: ['id', 'scope', 'size', 'created'], + description: 'Queue item field to sort by', + }, + sortDir: { + type: 'string', + enum: ['asc', 'desc'], + description: 'Sort direction', + }, + offset: { type: 'number', minimum: 0, description: 'Number of items to skip' }, + limit: { type: 'number', minimum: 1, description: 'Maximum number of items to return' }, + }, + additionalProperties: false, + }, }, required: ['hookId'], }, examples: [{ hookId: 11 }], - execute: async (make: Make, args: { hookId: number; from?: number; to?: number }) => { - return await make.hooks.incomings.list(args.hookId, { from: args.from, to: args.to }); + execute: async ( + make: Make, + args: { hookId: number; from?: number; to?: number; pg?: ListHookIncomingsOptions['pg'] }, + ) => { + return await make.hooks.incomings.list(args.hookId, { + from: args.from, + to: args.to, + pg: args.pg, + }); }, }, { diff --git a/test/hook-incomings-tools.spec.ts b/test/hook-incomings-tools.spec.ts index 62e4ece..c0d5a98 100644 --- a/test/hook-incomings-tools.spec.ts +++ b/test/hook-incomings-tools.spec.ts @@ -32,6 +32,30 @@ describe('MCP tools: hook-incomings', () => { expect(result).toStrictEqual(listMock.incomings); }); + it('Should expose and forward hook-incomings_list pagination', async () => { + const pg = { limit: 10, offset: 20, sortBy: 'created', sortDir: 'desc' } as const; + mockFetch( + `GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings?pg%5Blimit%5D=10&pg%5Boffset%5D=20&pg%5BsortBy%5D=created&pg%5BsortDir%5D=desc`, + listMock, + ); + + const tool = getTool('hook-incomings_list'); + const result = await tool.execute(make, { hookId: HOOK_ID, pg }); + + expect(tool.inputSchema.properties?.pg).toEqual( + expect.objectContaining({ + type: 'object', + properties: expect.objectContaining({ + limit: expect.objectContaining({ type: 'number' }), + offset: expect.objectContaining({ type: 'number' }), + sortBy: expect.objectContaining({ type: 'string' }), + sortDir: expect.objectContaining({ type: 'string' }), + }), + }), + ); + expect(result).toStrictEqual(listMock.incomings); + }); + it('Should execute hook-incomings_stats', async () => { mockFetch(`GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings/stats`, statsMock); From 81dd3bf5be277e05a2416567f5c80a05acf2ebae Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 17:38:26 +0200 Subject: [PATCH 14/16] fix(hook-incomings): align queue contracts Constrain pagination to the web API's created-only sorting and 10,000-item limit, including runtime and tool-schema validation. Isolate the live bulk-delete test by preserving every queued item except its own target. --- src/endpoints/hook-incomings.tools.ts | 13 +++++--- src/endpoints/hook-incomings.ts | 40 ++++++++++++++++++++++--- src/tools.ts | 4 +-- test/hook-incomings-tools.spec.ts | 8 ++--- test/hook-incomings.integration.test.ts | 13 +++++--- test/hook-incomings.spec.ts | 30 ++++++++++++++++++- 6 files changed, 89 insertions(+), 19 deletions(-) diff --git a/src/endpoints/hook-incomings.tools.ts b/src/endpoints/hook-incomings.tools.ts index 7dcb0f7..f73f187 100644 --- a/src/endpoints/hook-incomings.tools.ts +++ b/src/endpoints/hook-incomings.tools.ts @@ -35,16 +35,21 @@ export const tools: MakeTool[] = [ properties: { sortBy: { type: 'string', - enum: ['id', 'scope', 'size', 'created'], - description: 'Queue item field to sort by', + enum: ['created'], + description: 'Sort queue items by their creation time', }, sortDir: { type: 'string', enum: ['asc', 'desc'], description: 'Sort direction', }, - offset: { type: 'number', minimum: 0, description: 'Number of items to skip' }, - limit: { type: 'number', minimum: 1, description: 'Maximum number of items to return' }, + offset: { type: 'integer', minimum: 0, description: 'Number of items to skip' }, + limit: { + type: 'integer', + minimum: 1, + maximum: 10_000, + description: 'Maximum number of items to return (up to 10,000)', + }, }, additionalProperties: false, }, diff --git a/src/endpoints/hook-incomings.ts b/src/endpoints/hook-incomings.ts index 7d7e941..9b529fc 100644 --- a/src/endpoints/hook-incomings.ts +++ b/src/endpoints/hook-incomings.ts @@ -1,6 +1,7 @@ -import type { FetchFunction, JSONValue, Pagination } from '../types.js'; +import type { FetchFunction, JSONValue } from '../types.js'; const HOOK_INCOMING_ID_PATTERN = /^[0-9a-f]{32}$/; +const MAX_HOOK_INCOMINGS_LIMIT = 10_000; /** * A single item in a webhook's processing queue. @@ -18,6 +19,20 @@ export type HookIncoming = { created: string; }; +/** + * Pagination supported by the webhook processing queue endpoint. + */ +type HookIncomingsPagination = { + /** Queue items can only be sorted by their creation time */ + sortBy: 'created'; + /** Sort direction (ascending or descending) */ + sortDir: 'asc' | 'desc'; + /** Number of queue items to skip */ + offset: number; + /** Maximum number of queue items to return, up to 10,000 */ + limit: number; +}; + /** * Options for listing a webhook's queued incoming items. */ @@ -26,8 +41,8 @@ export type ListHookIncomingsOptions = { from?: number; /** Only include items queued at or before this Unix timestamp (ms) */ to?: number; - /** Pagination options */ - pg?: Partial>; + /** Pagination options supported by the webhook queue */ + pg?: Partial; }; /** @@ -37,7 +52,7 @@ type ListHookIncomingsResponse = { /** Queue items matching the query */ incomings: HookIncoming[]; /** Pagination information */ - pg: Partial>; + pg: Partial; }; /** @@ -153,8 +168,25 @@ export class HookIncomings { * @param hookId The hook ID to list queued items for * @param options Optional filtering and pagination parameters * @returns Promise with the list of queued items + * @throws {TypeError} If any pagination option is unsupported */ async list(hookId: number, options?: ListHookIncomingsOptions): Promise { + if (options?.pg?.sortBy !== undefined && options.pg.sortBy !== 'created') { + throw new TypeError('`pg.sortBy` must be `created` when specified'); + } + if (options?.pg?.sortDir !== undefined && options.pg.sortDir !== 'asc' && options.pg.sortDir !== 'desc') { + throw new TypeError('`pg.sortDir` must be `asc` or `desc` when specified'); + } + if ( + options?.pg?.limit !== undefined && + (!Number.isInteger(options.pg.limit) || options.pg.limit < 1 || options.pg.limit > MAX_HOOK_INCOMINGS_LIMIT) + ) { + throw new TypeError('`pg.limit` must be an integer between 1 and 10000 when specified'); + } + if (options?.pg?.offset !== undefined && (!Number.isInteger(options.pg.offset) || options.pg.offset < 0)) { + throw new TypeError('`pg.offset` must be a non-negative integer when specified'); + } + return ( await this.#fetch(`/hooks/${hookId}/incomings`, { query: { diff --git a/src/tools.ts b/src/tools.ts index b6872c1..c4dc531 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -43,11 +43,11 @@ import { tools as CustomRolesTools } from './endpoints/custom-roles.tools.js'; */ export type JSONSchema = { /** - * The type of the schema (object, string, number, boolean, array, etc.). + * The type of the schema (object, string, number, integer, boolean, array, etc.). * Optional when the schema is expressed purely through composition * (`oneOf`/`anyOf`/`allOf`) or a `const` value. */ - type?: 'object' | 'string' | 'number' | 'boolean' | 'array' | 'null'; + type?: 'object' | 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'null'; /** Properties definition for object types */ properties?: Record; /** Schemas for properties whose names match a regex pattern (object types) */ diff --git a/test/hook-incomings-tools.spec.ts b/test/hook-incomings-tools.spec.ts index c0d5a98..74bbd33 100644 --- a/test/hook-incomings-tools.spec.ts +++ b/test/hook-incomings-tools.spec.ts @@ -46,10 +46,10 @@ describe('MCP tools: hook-incomings', () => { expect.objectContaining({ type: 'object', properties: expect.objectContaining({ - limit: expect.objectContaining({ type: 'number' }), - offset: expect.objectContaining({ type: 'number' }), - sortBy: expect.objectContaining({ type: 'string' }), - sortDir: expect.objectContaining({ type: 'string' }), + limit: expect.objectContaining({ type: 'integer', minimum: 1, maximum: 10_000 }), + offset: expect.objectContaining({ type: 'integer', minimum: 0 }), + sortBy: expect.objectContaining({ type: 'string', enum: ['created'] }), + sortDir: expect.objectContaining({ type: 'string', enum: ['asc', 'desc'] }), }), }), ); diff --git a/test/hook-incomings.integration.test.ts b/test/hook-incomings.integration.test.ts index d7091ed..8128ade 100644 --- a/test/hook-incomings.integration.test.ts +++ b/test/hook-incomings.integration.test.ts @@ -150,17 +150,22 @@ function payloadMarker(data: HookIncomingDetail['data']): string | undefined { expect([...baselineIds].every(baselineId => remainingIds.has(baselineId))).toBe(true); }); - it('Should bulk-delete the other fixture delivery with confirmation while preserving the baseline', async () => { + it('Should bulk-delete only the other fixture delivery with confirmation', async () => { const id = requireTestId(TEST_MARKERS[1]); + const currentBeforeDelete = await listAll(); + const idsToPreserve = currentBeforeDelete.map(incoming => incoming.id).filter(currentId => currentId !== id); + + expect(currentBeforeDelete.some(incoming => incoming.id === id)).toBe(true); + const result = await make.hooks.incomings.delete(MAKE_HOOK_INCOMINGS_HOOK_ID, { all: true, confirmed: true, - exceptIds: [...baselineIds], + exceptIds: idsToPreserve, }); const remainingIds = new Set((await listAll()).map(incoming => incoming.id)); - expect(result.deletedIds).toContain(id); + expect(result.deletedIds).toStrictEqual([id]); expect(remainingIds.has(id)).toBe(false); - expect([...baselineIds].every(baselineId => remainingIds.has(baselineId))).toBe(true); + expect(idsToPreserve.every(preservedId => remainingIds.has(preservedId))).toBe(true); }); }); diff --git a/test/hook-incomings.spec.ts b/test/hook-incomings.spec.ts index 0a48ad7..ca4364a 100644 --- a/test/hook-incomings.spec.ts +++ b/test/hook-incomings.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from '@jest/globals'; import { Make } from '../src/make.js'; -import type { DeleteHookIncomingsOptions } from '../src/endpoints/hook-incomings.js'; +import type { DeleteHookIncomingsOptions, ListHookIncomingsOptions } from '../src/endpoints/hook-incomings.js'; import { mockFetch } from './test.utils.js'; import * as listMock from './mocks/hook-incomings/list.json'; @@ -40,6 +40,34 @@ describe('Endpoints: HookIncomings', () => { expect(result).toStrictEqual(listMock.incomings); }); + it('Should reject an unsupported list sort field', async () => { + const options = { pg: { sortBy: 'id' } } as unknown as ListHookIncomingsOptions; + + await expect(make.hooks.incomings.list(HOOK_ID, options)).rejects.toThrow( + '`pg.sortBy` must be `created` when specified', + ); + }); + + it('Should reject an unsupported list sort direction', async () => { + const options = { pg: { sortDir: 'sideways' } } as unknown as ListHookIncomingsOptions; + + await expect(make.hooks.incomings.list(HOOK_ID, options)).rejects.toThrow( + '`pg.sortDir` must be `asc` or `desc` when specified', + ); + }); + + it.each([0, 1.5, 10_001])('Should reject an unsupported list limit: %s', async limit => { + await expect(make.hooks.incomings.list(HOOK_ID, { pg: { limit } })).rejects.toThrow( + '`pg.limit` must be an integer between 1 and 10000 when specified', + ); + }); + + it.each([-1, 1.5])('Should reject an unsupported list offset: %s', async offset => { + await expect(make.hooks.incomings.list(HOOK_ID, { pg: { offset } })).rejects.toThrow( + '`pg.offset` must be a non-negative integer when specified', + ); + }); + it('Should get queue stats for a hook', async () => { mockFetch(`GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings/stats`, statsMock); From aaf2f5da65754976e509661435422aa3b0944216 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 17:49:36 +0200 Subject: [PATCH 15/16] fix(hook-incomings): validate timestamp filters Match the web API's integer Unix-millisecond contract for from/to in both direct SDK calls and tool schemas. --- src/endpoints/hook-incomings.tools.ts | 4 ++-- src/endpoints/hook-incomings.ts | 8 +++++++- test/hook-incomings-tools.spec.ts | 7 +++++++ test/hook-incomings.spec.ts | 9 +++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/endpoints/hook-incomings.tools.ts b/src/endpoints/hook-incomings.tools.ts index f73f187..5a48d3e 100644 --- a/src/endpoints/hook-incomings.tools.ts +++ b/src/endpoints/hook-incomings.tools.ts @@ -22,11 +22,11 @@ export const tools: MakeTool[] = [ properties: { hookId: { type: 'number', description: 'The hook ID to list queued items for' }, from: { - type: 'number', + type: 'integer', description: 'Only include items queued at or after this Unix timestamp (ms)', }, to: { - type: 'number', + type: 'integer', description: 'Only include items queued at or before this Unix timestamp (ms)', }, pg: { diff --git a/src/endpoints/hook-incomings.ts b/src/endpoints/hook-incomings.ts index 9b529fc..d096624 100644 --- a/src/endpoints/hook-incomings.ts +++ b/src/endpoints/hook-incomings.ts @@ -168,9 +168,15 @@ export class HookIncomings { * @param hookId The hook ID to list queued items for * @param options Optional filtering and pagination parameters * @returns Promise with the list of queued items - * @throws {TypeError} If any pagination option is unsupported + * @throws {TypeError} If a timestamp is not an integer or any pagination option is unsupported */ async list(hookId: number, options?: ListHookIncomingsOptions): Promise { + if (options?.from !== undefined && !Number.isInteger(options.from)) { + throw new TypeError('`from` must be an integer Unix timestamp in milliseconds when specified'); + } + if (options?.to !== undefined && !Number.isInteger(options.to)) { + throw new TypeError('`to` must be an integer Unix timestamp in milliseconds when specified'); + } if (options?.pg?.sortBy !== undefined && options.pg.sortBy !== 'created') { throw new TypeError('`pg.sortBy` must be `created` when specified'); } diff --git a/test/hook-incomings-tools.spec.ts b/test/hook-incomings-tools.spec.ts index 74bbd33..4a62461 100644 --- a/test/hook-incomings-tools.spec.ts +++ b/test/hook-incomings-tools.spec.ts @@ -56,6 +56,13 @@ describe('MCP tools: hook-incomings', () => { expect(result).toStrictEqual(listMock.incomings); }); + it('Should constrain hook-incomings_list timestamps to integers', () => { + const tool = getTool('hook-incomings_list'); + + expect(tool.inputSchema.properties?.from).toEqual(expect.objectContaining({ type: 'integer' })); + expect(tool.inputSchema.properties?.to).toEqual(expect.objectContaining({ type: 'integer' })); + }); + it('Should execute hook-incomings_stats', async () => { mockFetch(`GET https://make.local/api/v2/hooks/${HOOK_ID}/incomings/stats`, statsMock); diff --git a/test/hook-incomings.spec.ts b/test/hook-incomings.spec.ts index ca4364a..faf5153 100644 --- a/test/hook-incomings.spec.ts +++ b/test/hook-incomings.spec.ts @@ -40,6 +40,15 @@ describe('Endpoints: HookIncomings', () => { expect(result).toStrictEqual(listMock.incomings); }); + it.each([ + { field: 'from', options: { from: 1000.5 } }, + { field: 'to', options: { to: 2000.5 } }, + ])('Should reject a fractional $field list timestamp', async ({ field, options }) => { + await expect(make.hooks.incomings.list(HOOK_ID, options)).rejects.toThrow( + `\`${field}\` must be an integer Unix timestamp in milliseconds when specified`, + ); + }); + it('Should reject an unsupported list sort field', async () => { const options = { pg: { sortBy: 'id' } } as unknown as ListHookIncomingsOptions; From 80ab45cc7a8610e15a0e35d94351280940743e3c Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Tue, 1 Sep 2026 20:53:59 +0200 Subject: [PATCH 16/16] chore: bump version to 1.6.15 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc0e78f..bcb712a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@makehq/sdk", - "version": "1.6.14", + "version": "1.6.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@makehq/sdk", - "version": "1.6.14", + "version": "1.6.15", "license": "MIT", "devDependencies": { "@eslint/js": "^9.22.0", diff --git a/package.json b/package.json index 9361bae..83267a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makehq/sdk", - "version": "1.6.14", + "version": "1.6.15", "description": "Make TypeScript SDK", "license": "MIT", "author": "Make",