@doist/todoist-sdk 9.2.0 → 9.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/cjs/clients/app-client.js +17 -17
  2. package/dist/cjs/clients/comment-client.js +3 -4
  3. package/dist/cjs/clients/folder-client.js +3 -4
  4. package/dist/cjs/clients/insights-client.js +6 -7
  5. package/dist/cjs/clients/label-client.js +3 -4
  6. package/dist/cjs/clients/project-client.js +8 -9
  7. package/dist/cjs/clients/section-client.js +5 -6
  8. package/dist/cjs/clients/task-client.js +6 -7
  9. package/dist/cjs/clients/ui-extension-client.js +4 -5
  10. package/dist/cjs/clients/workspace-client.js +3 -4
  11. package/dist/cjs/types/index.js +1 -0
  12. package/dist/cjs/types/webhooks/envelope.js +26 -0
  13. package/dist/cjs/types/webhooks/index.js +19 -0
  14. package/dist/cjs/types/webhooks/tasks.js +68 -0
  15. package/dist/cjs/types/webhooks/types.js +34 -0
  16. package/dist/cjs/utils/index.js +4 -1
  17. package/dist/cjs/utils/validators.js +7 -2
  18. package/dist/cjs/utils/webhook-parser.js +60 -0
  19. package/dist/esm/clients/app-client.js +18 -18
  20. package/dist/esm/clients/comment-client.js +4 -5
  21. package/dist/esm/clients/folder-client.js +4 -5
  22. package/dist/esm/clients/insights-client.js +7 -8
  23. package/dist/esm/clients/label-client.js +4 -5
  24. package/dist/esm/clients/project-client.js +9 -10
  25. package/dist/esm/clients/section-client.js +6 -7
  26. package/dist/esm/clients/task-client.js +7 -8
  27. package/dist/esm/clients/ui-extension-client.js +5 -6
  28. package/dist/esm/clients/workspace-client.js +4 -5
  29. package/dist/esm/types/index.js +1 -0
  30. package/dist/esm/types/webhooks/envelope.js +23 -0
  31. package/dist/esm/types/webhooks/index.js +3 -0
  32. package/dist/esm/types/webhooks/tasks.js +65 -0
  33. package/dist/esm/types/webhooks/types.js +31 -0
  34. package/dist/esm/utils/index.js +1 -0
  35. package/dist/esm/utils/validators.js +5 -0
  36. package/dist/esm/utils/webhook-parser.js +56 -0
  37. package/dist/types/types/index.d.ts +1 -0
  38. package/dist/types/types/webhooks/envelope.d.ts +34 -0
  39. package/dist/types/types/webhooks/index.d.ts +3 -0
  40. package/dist/types/types/webhooks/tasks.d.ts +981 -0
  41. package/dist/types/types/webhooks/types.d.ts +893 -0
  42. package/dist/types/utils/index.d.ts +2 -0
  43. package/dist/types/utils/validators.d.ts +2 -0
  44. package/dist/types/utils/webhook-parser.d.ts +48 -0
  45. package/package.json +1 -1
@@ -1,8 +1,7 @@
1
- import { z } from 'zod';
2
1
  import { ENDPOINT_REST_SECTIONS, ENDPOINT_REST_SECTIONS_SEARCH, SECTION_ARCHIVE, SECTION_UNARCHIVE, } from '../consts/endpoints.js';
3
2
  import { isSuccess, request } from '../transport/http-client.js';
4
3
  import { generatePath } from '../utils/request-helpers.js';
5
- import { validateSection, validateSectionArray } from '../utils/validators.js';
4
+ import { IdSchema, validateSection, validateSectionArray } from '../utils/validators.js';
6
5
  import { BaseClient } from './base-client.js';
7
6
  /**
8
7
  * Internal sub-client handling all section-domain endpoints.
@@ -40,7 +39,7 @@ export class SectionClient extends BaseClient {
40
39
  };
41
40
  }
42
41
  async getSection(id) {
43
- z.string().min(1).parse(id);
42
+ IdSchema.parse(id);
44
43
  const response = await request({
45
44
  httpMethod: 'GET',
46
45
  baseUri: this.syncApiBase,
@@ -63,7 +62,7 @@ export class SectionClient extends BaseClient {
63
62
  return validateSection(response.data);
64
63
  }
65
64
  async updateSection(id, args, requestId) {
66
- z.string().min(1).parse(id);
65
+ IdSchema.parse(id);
67
66
  const response = await request({
68
67
  httpMethod: 'POST',
69
68
  baseUri: this.syncApiBase,
@@ -76,7 +75,7 @@ export class SectionClient extends BaseClient {
76
75
  return validateSection(response.data);
77
76
  }
78
77
  async deleteSection(id, requestId) {
79
- z.string().min(1).parse(id);
78
+ IdSchema.parse(id);
80
79
  const response = await request({
81
80
  httpMethod: 'DELETE',
82
81
  baseUri: this.syncApiBase,
@@ -88,7 +87,7 @@ export class SectionClient extends BaseClient {
88
87
  return isSuccess(response);
89
88
  }
90
89
  async archiveSection(id, requestId) {
91
- z.string().min(1).parse(id);
90
+ IdSchema.parse(id);
92
91
  const response = await request({
93
92
  httpMethod: 'POST',
94
93
  baseUri: this.syncApiBase,
@@ -100,7 +99,7 @@ export class SectionClient extends BaseClient {
100
99
  return validateSection(response.data);
101
100
  }
102
101
  async unarchiveSection(id, requestId) {
103
- z.string().min(1).parse(id);
102
+ IdSchema.parse(id);
104
103
  const response = await request({
105
104
  httpMethod: 'POST',
106
105
  baseUri: this.syncApiBase,
@@ -10,14 +10,13 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { v4 as uuidv4 } from 'uuid';
13
- import { z } from 'zod';
14
13
  import { ENDPOINT_REST_TASKS, ENDPOINT_REST_TASKS_COMPLETED, ENDPOINT_REST_TASKS_COMPLETED_BY_COMPLETION_DATE, ENDPOINT_REST_TASKS_COMPLETED_BY_DUE_DATE, ENDPOINT_REST_TASKS_COMPLETED_SEARCH, ENDPOINT_REST_TASKS_FILTER, ENDPOINT_REST_TASK_CLOSE, ENDPOINT_REST_TASK_MOVE, ENDPOINT_REST_TASK_REOPEN, ENDPOINT_SYNC_QUICK_ADD, } from '../consts/endpoints.js';
15
14
  import { isSuccess, request } from '../transport/http-client.js';
16
15
  import { performSyncRequest } from '../transport/sync-request.js';
17
16
  import { TodoistRequestError } from '../types/index.js';
18
17
  import { generatePath, spreadIfDefined } from '../utils/request-helpers.js';
19
18
  import { processTaskContent } from '../utils/uncompletable-helpers.js';
20
- import { validateTask, validateTaskArray } from '../utils/validators.js';
19
+ import { IdSchema, validateTask, validateTaskArray } from '../utils/validators.js';
21
20
  import { BaseClient } from './base-client.js';
22
21
  const MAX_COMMAND_COUNT = 100;
23
22
  /**
@@ -28,7 +27,7 @@ const MAX_COMMAND_COUNT = 100;
28
27
  */
29
28
  export class TaskClient extends BaseClient {
30
29
  async getTask(id) {
31
- z.string().min(1).parse(id);
30
+ IdSchema.parse(id);
32
31
  const response = await request({
33
32
  httpMethod: 'GET',
34
33
  baseUri: this.syncApiBase,
@@ -152,7 +151,7 @@ export class TaskClient extends BaseClient {
152
151
  return validateTask(response.data);
153
152
  }
154
153
  async updateTask(id, args, requestId) {
155
- z.string().min(1).parse(id);
154
+ IdSchema.parse(id);
156
155
  // Translate SDK alias for due-date clearing to Todoist's accepted payload value.
157
156
  const normalizedArgs = args.dueString === null ? Object.assign(Object.assign({}, args), { dueString: 'no date' }) : args;
158
157
  // Process content if both content and isUncompletable are provided
@@ -200,7 +199,7 @@ export class TaskClient extends BaseClient {
200
199
  return validateTaskArray(syncTasks);
201
200
  }
202
201
  async moveTask(id, args, requestId) {
203
- z.string().min(1).parse(id);
202
+ IdSchema.parse(id);
204
203
  const response = await request({
205
204
  httpMethod: 'POST',
206
205
  baseUri: this.syncApiBase,
@@ -213,7 +212,7 @@ export class TaskClient extends BaseClient {
213
212
  return validateTask(response.data);
214
213
  }
215
214
  async closeTask(id, requestId) {
216
- z.string().min(1).parse(id);
215
+ IdSchema.parse(id);
217
216
  const response = await request({
218
217
  httpMethod: 'POST',
219
218
  baseUri: this.syncApiBase,
@@ -225,7 +224,7 @@ export class TaskClient extends BaseClient {
225
224
  return isSuccess(response);
226
225
  }
227
226
  async reopenTask(id, requestId) {
228
- z.string().min(1).parse(id);
227
+ IdSchema.parse(id);
229
228
  const response = await request({
230
229
  httpMethod: 'POST',
231
230
  baseUri: this.syncApiBase,
@@ -237,7 +236,7 @@ export class TaskClient extends BaseClient {
237
236
  return isSuccess(response);
238
237
  }
239
238
  async deleteTask(id, requestId) {
240
- z.string().min(1).parse(id);
239
+ IdSchema.parse(id);
241
240
  const response = await request({
242
241
  httpMethod: 'DELETE',
243
242
  baseUri: this.syncApiBase,
@@ -1,9 +1,8 @@
1
- import { z } from 'zod';
2
1
  import { ENDPOINT_REST_APPS_UI_EXTENSIONS, ENDPOINT_REST_APPS_UI_EXTENSIONS_INSTALLED, getUiExtensionEndpoint, getUiExtensionIconEndpoint, getUiExtensionsByIntegrationEndpoint, } from '../consts/endpoints.js';
3
2
  import { isSuccess, request } from '../transport/http-client.js';
4
3
  import { camelCaseKeys } from '../utils/case-conversion.js';
5
4
  import { uploadMultipartFile } from '../utils/multipart-upload.js';
6
- import { validateUiExtension, validateUiExtensionArray } from '../utils/validators.js';
5
+ import { IdSchema, validateUiExtension, validateUiExtensionArray } from '../utils/validators.js';
7
6
  import { BaseClient } from './base-client.js';
8
7
  /**
9
8
  * Internal sub-client handling all UI-extension endpoints for developer
@@ -14,7 +13,7 @@ import { BaseClient } from './base-client.js';
14
13
  */
15
14
  export class UiExtensionClient extends BaseClient {
16
15
  async getUiExtension(uiExtensionId, requestId) {
17
- z.string().min(1).parse(uiExtensionId);
16
+ IdSchema.parse(uiExtensionId);
18
17
  const response = await request({
19
18
  httpMethod: 'GET',
20
19
  baseUri: this.apiRootBase,
@@ -37,7 +36,7 @@ export class UiExtensionClient extends BaseClient {
37
36
  return validateUiExtensionArray(response.data);
38
37
  }
39
38
  async getUiExtensionsForApp(appId, requestId) {
40
- z.string().min(1).parse(appId);
39
+ IdSchema.parse(appId);
41
40
  const response = await request({
42
41
  httpMethod: 'GET',
43
42
  baseUri: this.apiRootBase,
@@ -61,7 +60,7 @@ export class UiExtensionClient extends BaseClient {
61
60
  return validateUiExtension(response.data);
62
61
  }
63
62
  async updateUiExtension(uiExtensionId, args, requestId) {
64
- z.string().min(1).parse(uiExtensionId);
63
+ IdSchema.parse(uiExtensionId);
65
64
  const response = await request({
66
65
  httpMethod: 'PATCH',
67
66
  baseUri: this.apiRootBase,
@@ -74,7 +73,7 @@ export class UiExtensionClient extends BaseClient {
74
73
  return validateUiExtension(response.data);
75
74
  }
76
75
  async deleteUiExtension(uiExtensionId, requestId) {
77
- z.string().min(1).parse(uiExtensionId);
76
+ IdSchema.parse(uiExtensionId);
78
77
  const response = await request({
79
78
  httpMethod: 'DELETE',
80
79
  baseUri: this.apiRootBase,
@@ -9,12 +9,11 @@ var __rest = (this && this.__rest) || function (s, e) {
9
9
  }
10
10
  return t;
11
11
  };
12
- import { z } from 'zod';
13
12
  import { ENDPOINT_REST_WORKSPACES, ENDPOINT_WORKSPACE_INVITATIONS, ENDPOINT_WORKSPACE_INVITATIONS_ALL, ENDPOINT_WORKSPACE_INVITATIONS_DELETE, ENDPOINT_WORKSPACE_JOIN, ENDPOINT_WORKSPACE_LOGO, ENDPOINT_WORKSPACE_MEMBERS, ENDPOINT_WORKSPACE_PLAN_DETAILS, ENDPOINT_WORKSPACE_USERS, getWorkspaceActiveProjectsEndpoint, getWorkspaceArchivedProjectsEndpoint, getWorkspaceInvitationAcceptEndpoint, getWorkspaceInvitationRejectEndpoint, getWorkspaceInviteUsersEndpoint, getWorkspaceUserEndpoint, getWorkspaceUserTasksEndpoint, } from '../consts/endpoints.js';
14
13
  import { isSuccess, request } from '../transport/http-client.js';
15
14
  import { uploadMultipartFile } from '../utils/multipart-upload.js';
16
15
  import { generatePath } from '../utils/request-helpers.js';
17
- import { validateJoinWorkspaceResult, validateMemberActivityInfoArray, validateProject, validateWorkspace, validateWorkspaceArray, validateWorkspaceInvitation, validateWorkspaceInvitationArray, validateWorkspacePlanDetails, validateWorkspaceUserArray, validateWorkspaceUserTaskArray, } from '../utils/validators.js';
16
+ import { IdSchema, validateJoinWorkspaceResult, validateMemberActivityInfoArray, validateProject, validateWorkspace, validateWorkspaceArray, validateWorkspaceInvitation, validateWorkspaceInvitationArray, validateWorkspacePlanDetails, validateWorkspaceUserArray, validateWorkspaceUserTaskArray, } from '../utils/validators.js';
18
17
  import { BaseClient } from './base-client.js';
19
18
  /**
20
19
  * Internal sub-client handling all workspace-domain endpoints —
@@ -196,7 +195,7 @@ export class WorkspaceClient extends BaseClient {
196
195
  return validateWorkspaceArray(response.data);
197
196
  }
198
197
  async getWorkspace(id, requestId) {
199
- z.string().min(1).parse(id);
198
+ IdSchema.parse(id);
200
199
  const response = await request({
201
200
  httpMethod: 'GET',
202
201
  baseUri: this.syncApiBase,
@@ -220,7 +219,7 @@ export class WorkspaceClient extends BaseClient {
220
219
  return validateWorkspace(response.data);
221
220
  }
222
221
  async updateWorkspace(id, args, requestId) {
223
- z.string().min(1).parse(id);
222
+ IdSchema.parse(id);
224
223
  const response = await request({
225
224
  httpMethod: 'POST',
226
225
  baseUri: this.syncApiBase,
@@ -233,7 +232,7 @@ export class WorkspaceClient extends BaseClient {
233
232
  return validateWorkspace(response.data);
234
233
  }
235
234
  async deleteWorkspace(id, requestId) {
236
- z.string().min(1).parse(id);
235
+ IdSchema.parse(id);
237
236
  const response = await request({
238
237
  httpMethod: 'DELETE',
239
238
  baseUri: this.syncApiBase,
@@ -20,3 +20,4 @@ export * from './http.js';
20
20
  export * from './sync/index.js';
21
21
  export * from './apps/index.js';
22
22
  export * from './ui-extensions/index.js';
23
+ export * from './webhooks/index.js';
@@ -0,0 +1,23 @@
1
+ import { z } from 'zod';
2
+ import { WebhookVersionSchema } from '../apps/types.js';
3
+ /**
4
+ * Collaborator info for the user that triggered a webhook event.
5
+ * @see https://developer.todoist.com/sync/v1/#webhooks
6
+ */
7
+ export const WebhookInitiatorSchema = z.object({
8
+ id: z.string(),
9
+ fullName: z.string(),
10
+ email: z.string(),
11
+ imageId: z.string().nullable(),
12
+ isPremium: z.boolean(),
13
+ });
14
+ /**
15
+ * Fields common to every webhook payload, regardless of `eventName`.
16
+ * Meant to be spread into per-event variant schemas.
17
+ */
18
+ export const BaseWebhookEnvelopeShape = {
19
+ version: WebhookVersionSchema,
20
+ userId: z.string(),
21
+ initiator: WebhookInitiatorSchema,
22
+ triggeredAt: z.coerce.date(),
23
+ };
@@ -0,0 +1,3 @@
1
+ export * from './envelope.js';
2
+ export * from './tasks.js';
3
+ export * from './types.js';
@@ -0,0 +1,65 @@
1
+ import { z } from 'zod';
2
+ import { TaskSchema } from '../tasks/types.js';
3
+ import { BaseWebhookEnvelopeShape } from './envelope.js';
4
+ /**
5
+ * Enum-like object of task (`item:*`) webhook event names. Single source of
6
+ * truth for the discriminator values — `TASK_WEBHOOK_EVENTS` and every
7
+ * `z.literal()` below derive from it.
8
+ */
9
+ export const TaskWebhookEventEnum = {
10
+ Added: 'item:added',
11
+ Updated: 'item:updated',
12
+ Completed: 'item:completed',
13
+ Uncompleted: 'item:uncompleted',
14
+ Deleted: 'item:deleted',
15
+ };
16
+ /**
17
+ * Webhook events that carry a {@link Task} as their `eventData`.
18
+ *
19
+ * The on-the-wire event names remain `item:*` for historical reasons; the
20
+ * SDK types call them `Task*` to match the rest of the public API where the
21
+ * resource is named `Task`.
22
+ */
23
+ export const TASK_WEBHOOK_EVENTS = [
24
+ TaskWebhookEventEnum.Added,
25
+ TaskWebhookEventEnum.Updated,
26
+ TaskWebhookEventEnum.Completed,
27
+ TaskWebhookEventEnum.Uncompleted,
28
+ TaskWebhookEventEnum.Deleted,
29
+ ];
30
+ /**
31
+ * Possible values for the `updateIntent` field on `item:updated`'s
32
+ * `eventDataExtra`. `item_updated` is the default; the others narrow the
33
+ * reason for the update (for example, a recurring completion or a change of
34
+ * assignee).
35
+ *
36
+ * @see https://developer.todoist.com/sync/v1/#webhooks (Events Extra)
37
+ */
38
+ export const UPDATE_INTENTS = [
39
+ 'item_updated',
40
+ 'item_completed',
41
+ 'item_uncompleted',
42
+ 'responsible_uid_changed',
43
+ ];
44
+ /**
45
+ * An unrecognised value from a future backend release degrades gracefully to
46
+ * `'item_updated'` rather than crashing the whole payload parse.
47
+ */
48
+ export const UpdateIntentSchema = z.enum(UPDATE_INTENTS).catch('item_updated');
49
+ /**
50
+ * Extra info delivered alongside an `item:updated` event when the initiator
51
+ * is the acting user. Contains the pre-update task state plus the reason for
52
+ * the update — useful for diffing or for distinguishing manual edits from
53
+ * recurring-task completions.
54
+ *
55
+ * @see https://developer.todoist.com/sync/v1/#webhooks (Events Extra)
56
+ */
57
+ export const TaskUpdatedExtraSchema = z.object({
58
+ oldItem: TaskSchema,
59
+ updateIntent: UpdateIntentSchema,
60
+ });
61
+ export const TaskAddedPayloadSchema = z.object(Object.assign(Object.assign({}, BaseWebhookEnvelopeShape), { eventName: z.literal(TaskWebhookEventEnum.Added), eventData: TaskSchema }));
62
+ export const TaskUpdatedPayloadSchema = z.object(Object.assign(Object.assign({}, BaseWebhookEnvelopeShape), { eventName: z.literal(TaskWebhookEventEnum.Updated), eventData: TaskSchema, eventDataExtra: TaskUpdatedExtraSchema.optional() }));
63
+ export const TaskCompletedPayloadSchema = z.object(Object.assign(Object.assign({}, BaseWebhookEnvelopeShape), { eventName: z.literal(TaskWebhookEventEnum.Completed), eventData: TaskSchema }));
64
+ export const TaskUncompletedPayloadSchema = z.object(Object.assign(Object.assign({}, BaseWebhookEnvelopeShape), { eventName: z.literal(TaskWebhookEventEnum.Uncompleted), eventData: TaskSchema }));
65
+ export const TaskDeletedPayloadSchema = z.object(Object.assign(Object.assign({}, BaseWebhookEnvelopeShape), { eventName: z.literal(TaskWebhookEventEnum.Deleted), eventData: TaskSchema }));
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ import { WEBHOOK_EVENTS, WebhookEventSchema } from '../apps/types.js';
3
+ import { BaseWebhookEnvelopeShape } from './envelope.js';
4
+ import { TASK_WEBHOOK_EVENTS, TaskAddedPayloadSchema, TaskCompletedPayloadSchema, TaskDeletedPayloadSchema, TaskUncompletedPayloadSchema, TaskUpdatedPayloadSchema, } from './tasks.js';
5
+ const untypedEventNames = WEBHOOK_EVENTS.filter((event) => !TASK_WEBHOOK_EVENTS.includes(event));
6
+ /**
7
+ * A single branch covering every event whose payload has not yet been typed
8
+ * per resource. Follow-up PRs will replace slices of this with dedicated
9
+ * variants (notes, projects, sections, labels, filters, reminders).
10
+ */
11
+ export const UntypedWebhookPayloadSchema = z.object(Object.assign(Object.assign({}, BaseWebhookEnvelopeShape), { eventName: z.enum(untypedEventNames), eventData: z.unknown(), eventDataExtra: z.unknown().optional() }));
12
+ /**
13
+ * Wrapper schema for an incoming webhook payload.
14
+ *
15
+ * The backend sends snake_case keys and uses the public event form
16
+ * (`item:added`) in the `event_name` field, so consumers using
17
+ * `parseWebhookPayload` receive camelCase fields directly.
18
+ *
19
+ * Typed today: `item:*` events carry a parsed {@link Task}. Other events
20
+ * still expose `eventData` as `unknown`; they will be narrowed in follow-up
21
+ * PRs (notes, projects, sections, labels, filters, reminders).
22
+ */
23
+ export const WebhookPayloadSchema = z.discriminatedUnion('eventName', [
24
+ TaskAddedPayloadSchema,
25
+ TaskUpdatedPayloadSchema,
26
+ TaskCompletedPayloadSchema,
27
+ TaskUncompletedPayloadSchema,
28
+ TaskDeletedPayloadSchema,
29
+ UntypedWebhookPayloadSchema,
30
+ ]);
31
+ export { WebhookEventSchema };
@@ -3,3 +3,4 @@ export * from './sanitization.js';
3
3
  export { getTaskUrl, getProjectUrl, getSectionUrl } from './url-helpers.js';
4
4
  export { createCommand } from './sync-helpers.js';
5
5
  export * from './project-helpers.js';
6
+ export { parseWebhookPayload, verifyWebhookSignature } from './webhook-parser.js';
@@ -21,6 +21,11 @@ function createValidator(schema) {
21
21
  validateArray: (input) => z.array(schema).parse(input),
22
22
  };
23
23
  }
24
+ const TMP_ID_MESSAGE = 'This looks like a client-side placeholder — wait for sync to complete before calling the API.';
25
+ export const IdSchema = z
26
+ .string()
27
+ .min(1)
28
+ .refine((id) => !id.startsWith('tmp-'), { message: TMP_ID_MESSAGE });
24
29
  // Entity validators
25
30
  export const { validate: validateTask, validateArray: validateTaskArray } = createValidator(TaskSchema);
26
31
  /**
@@ -0,0 +1,56 @@
1
+ import { WebhookPayloadSchema } from '../types/webhooks/types.js';
2
+ import { camelCaseKeys } from './case-conversion.js';
3
+ /**
4
+ * Parses an incoming webhook request body into a typed payload.
5
+ *
6
+ * The backend sends snake_case keys (e.g. `event_name`, `user_id`, `added_at`);
7
+ * this converts them to camelCase and validates against
8
+ * {@link WebhookPayloadSchema}. Event-specific `eventData` is returned as
9
+ * `unknown` for events that are not yet narrowed per-resource.
10
+ *
11
+ * Pass the parsed JSON body (or any unknown value) — validation throws on
12
+ * unexpected shape.
13
+ */
14
+ export function parseWebhookPayload(raw) {
15
+ return WebhookPayloadSchema.parse(camelCaseKeys(raw));
16
+ }
17
+ const encoder = new TextEncoder();
18
+ function base64ToBytes(base64) {
19
+ const binary = atob(base64);
20
+ const bytes = new Uint8Array(binary.length);
21
+ for (let i = 0; i < binary.length; i++) {
22
+ bytes[i] = binary.charCodeAt(i);
23
+ }
24
+ return bytes;
25
+ }
26
+ /**
27
+ * Verifies a webhook request's HMAC signature.
28
+ *
29
+ * Todoist signs webhook deliveries with an HMAC-SHA256 of the raw request body
30
+ * using the app's `client_secret` as the key, base64-encoded in the
31
+ * `X-Todoist-Hmac-SHA256` header.
32
+ *
33
+ * Uses the Web Crypto API (`crypto.subtle`), which is available in Node ≥ 20,
34
+ * modern browsers, React Native (with polyfills), and edge runtimes. The
35
+ * comparison is constant-time.
36
+ *
37
+ * @returns `true` when the signature is valid, `false` otherwise. Never throws
38
+ * on malformed input.
39
+ */
40
+ export async function verifyWebhookSignature({ rawBody, signature, clientSecret, }) {
41
+ let provided;
42
+ try {
43
+ provided = base64ToBytes(signature);
44
+ }
45
+ catch (_a) {
46
+ return false;
47
+ }
48
+ const key = await crypto.subtle.importKey('raw', encoder.encode(clientSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
49
+ const body = typeof rawBody === 'string' ? encoder.encode(rawBody) : rawBody;
50
+ try {
51
+ return await crypto.subtle.verify('HMAC', key, provided, body);
52
+ }
53
+ catch (_b) {
54
+ return false;
55
+ }
56
+ }
@@ -20,3 +20,4 @@ export * from './http.js';
20
20
  export * from './sync/index.js';
21
21
  export * from './apps/index.js';
22
22
  export * from './ui-extensions/index.js';
23
+ export * from './webhooks/index.js';
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Collaborator info for the user that triggered a webhook event.
4
+ * @see https://developer.todoist.com/sync/v1/#webhooks
5
+ */
6
+ export declare const WebhookInitiatorSchema: z.ZodObject<{
7
+ id: z.ZodString;
8
+ fullName: z.ZodString;
9
+ email: z.ZodString;
10
+ imageId: z.ZodNullable<z.ZodString>;
11
+ isPremium: z.ZodBoolean;
12
+ }, z.core.$strip>;
13
+ /**
14
+ * Collaborator info for the user that triggered a webhook event.
15
+ */
16
+ export type WebhookInitiator = z.infer<typeof WebhookInitiatorSchema>;
17
+ /**
18
+ * Fields common to every webhook payload, regardless of `eventName`.
19
+ * Meant to be spread into per-event variant schemas.
20
+ */
21
+ export declare const BaseWebhookEnvelopeShape: {
22
+ readonly version: z.ZodEnum<{
23
+ 1: "1";
24
+ }>;
25
+ readonly userId: z.ZodString;
26
+ readonly initiator: z.ZodObject<{
27
+ id: z.ZodString;
28
+ fullName: z.ZodString;
29
+ email: z.ZodString;
30
+ imageId: z.ZodNullable<z.ZodString>;
31
+ isPremium: z.ZodBoolean;
32
+ }, z.core.$strip>;
33
+ readonly triggeredAt: z.ZodCoercedDate<unknown>;
34
+ };
@@ -0,0 +1,3 @@
1
+ export * from './envelope.js';
2
+ export * from './tasks.js';
3
+ export * from './types.js';