@doist/todoist-sdk 9.3.0 → 9.5.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 (31) hide show
  1. package/dist/cjs/test-utils/webhook-fixtures.js +32 -0
  2. package/dist/cjs/types/index.js +1 -0
  3. package/dist/cjs/types/webhooks/comments.js +96 -0
  4. package/dist/cjs/types/webhooks/envelope.js +26 -0
  5. package/dist/cjs/types/webhooks/index.js +21 -0
  6. package/dist/cjs/types/webhooks/labels.js +54 -0
  7. package/dist/cjs/types/webhooks/tasks.js +68 -0
  8. package/dist/cjs/types/webhooks/types.js +54 -0
  9. package/dist/cjs/utils/index.js +4 -1
  10. package/dist/cjs/utils/webhook-parser.js +60 -0
  11. package/dist/esm/test-utils/webhook-fixtures.js +26 -0
  12. package/dist/esm/types/index.js +1 -0
  13. package/dist/esm/types/webhooks/comments.js +93 -0
  14. package/dist/esm/types/webhooks/envelope.js +23 -0
  15. package/dist/esm/types/webhooks/index.js +5 -0
  16. package/dist/esm/types/webhooks/labels.js +51 -0
  17. package/dist/esm/types/webhooks/tasks.js +65 -0
  18. package/dist/esm/types/webhooks/types.js +51 -0
  19. package/dist/esm/utils/index.js +1 -0
  20. package/dist/esm/utils/webhook-parser.js +56 -0
  21. package/dist/types/test-utils/webhook-fixtures.d.ts +13 -0
  22. package/dist/types/types/index.d.ts +1 -0
  23. package/dist/types/types/webhooks/comments.d.ts +1600 -0
  24. package/dist/types/types/webhooks/envelope.d.ts +34 -0
  25. package/dist/types/types/webhooks/index.d.ts +5 -0
  26. package/dist/types/types/webhooks/labels.d.ts +160 -0
  27. package/dist/types/types/webhooks/tasks.d.ts +981 -0
  28. package/dist/types/types/webhooks/types.d.ts +1936 -0
  29. package/dist/types/utils/index.d.ts +2 -0
  30. package/dist/types/utils/webhook-parser.d.ts +48 -0
  31. package/package.json +1 -1
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CLIENT_SECRET = void 0;
4
+ exports.sign = sign;
5
+ exports.rawTask = rawTask;
6
+ exports.envelope = envelope;
7
+ const node_crypto_1 = require("node:crypto");
8
+ exports.CLIENT_SECRET = 'test-client-secret';
9
+ /** HMAC-SHA256 signs a raw body with the test client secret (or an override). */
10
+ function sign(body, secret = exports.CLIENT_SECRET) {
11
+ return (0, node_crypto_1.createHmac)('sha256', secret).update(body).digest('base64');
12
+ }
13
+ /**
14
+ * Snake_case task payload matching the v1 webhook wire shape.
15
+ */
16
+ function rawTask(overrides = {}) {
17
+ return Object.assign({ id: '6XR4GqQQCW6Gv9h4', user_id: '2671355', project_id: '6XR4H993xv8H5qCR', section_id: null, parent_id: null, added_by_uid: '2671355', assigned_by_uid: null, responsible_uid: null, labels: [], deadline: null, duration: null, is_collapsed: false, checked: false, is_deleted: false, added_at: '2025-02-10T10:33:38.000000Z', completed_at: null, completed_by_uid: null, updated_at: '2025-02-10T10:33:38.000000Z', due: null, priority: 1, child_order: 3, content: 'Buy Milk', description: '', note_count: 0, day_order: -1, goal_ids: [], url: 'https://app.todoist.com/app/task/6XR4GqQQCW6Gv9h4' }, overrides);
18
+ }
19
+ /**
20
+ * Wraps an event's `event_data` in the snake_case envelope the backend sends
21
+ * (initiator, version, timestamps, etc). `extra` passes extra top-level keys
22
+ * like `event_data_extra`.
23
+ */
24
+ function envelope(event, eventData, extra) {
25
+ return Object.assign({ version: '1', user_id: '2671355', initiator: {
26
+ id: '2671355',
27
+ full_name: 'Alice',
28
+ email: 'alice@example.com',
29
+ image_id: 'ad38375bdb094286af59f1eab36d8f20',
30
+ is_premium: true,
31
+ }, event_name: event, event_data: eventData, triggered_at: '2025-02-10T10:39:38.000000Z' }, extra);
32
+ }
@@ -36,3 +36,4 @@ __exportStar(require("./http"), exports);
36
36
  __exportStar(require("./sync"), exports);
37
37
  __exportStar(require("./apps"), exports);
38
38
  __exportStar(require("./ui-extensions"), exports);
39
+ __exportStar(require("./webhooks"), exports);
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.CommentDeletedPayloadSchema = exports.CommentUpdatedPayloadSchema = exports.CommentAddedPayloadSchema = exports.WebhookCommentSchema = exports.WebhookProjectCommentSchema = exports.WebhookItemCommentSchema = exports.COMMENT_WEBHOOK_EVENTS = exports.CommentWebhookEventEnum = void 0;
15
+ const zod_1 = require("zod");
16
+ const types_1 = require("../comments/types");
17
+ const types_2 = require("../tasks/types");
18
+ const envelope_1 = require("./envelope");
19
+ /**
20
+ * Enum-like object of comment (`note:*`) webhook event names. Single source
21
+ * of truth for the discriminator values — `COMMENT_WEBHOOK_EVENTS` and every
22
+ * `z.literal()` below derive from it.
23
+ *
24
+ * The on-the-wire event names remain `note:*` for historical reasons; the
25
+ * SDK types use `Comment*` to match the rest of the public API where the
26
+ * resource is named `Comment`.
27
+ */
28
+ exports.CommentWebhookEventEnum = {
29
+ Added: 'note:added',
30
+ Updated: 'note:updated',
31
+ Deleted: 'note:deleted',
32
+ };
33
+ /**
34
+ * Webhook events that carry a comment as their `eventData`.
35
+ */
36
+ exports.COMMENT_WEBHOOK_EVENTS = [
37
+ exports.CommentWebhookEventEnum.Added,
38
+ exports.CommentWebhookEventEnum.Updated,
39
+ exports.CommentWebhookEventEnum.Deleted,
40
+ ];
41
+ /**
42
+ * Fields common to both item and project comment payloads.
43
+ *
44
+ * `postedAt` and `postedUid` are nullable here (unlike
45
+ * {@link RawCommentSchema}) because webhook payloads may omit them — for
46
+ * example when the poster could not be resolved, or on a deleted-note
47
+ * tombstone.
48
+ */
49
+ const WebhookCommentBaseSchema = zod_1.z.object({
50
+ id: zod_1.z.string(),
51
+ content: zod_1.z.string(),
52
+ postedAt: zod_1.z.coerce.date().nullable(),
53
+ postedUid: zod_1.z.string().nullable(),
54
+ fileAttachment: types_1.AttachmentSchema.nullable(),
55
+ uidsToNotify: zod_1.z.array(zod_1.z.string()).nullable(),
56
+ reactions: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).nullable(),
57
+ isDeleted: zod_1.z.boolean(),
58
+ });
59
+ /**
60
+ * A comment attached to a task, delivered in a webhook payload.
61
+ *
62
+ * Webhook item-comment payloads carry two fields that don't appear on the
63
+ * REST comment resource: `url` (the direct task URL) and `item` (the full
64
+ * embedded {@link Task}). Both are parsed here so consumers handling a
65
+ * `note:added` event also get the task it belongs to.
66
+ *
67
+ * `itemId` is exposed as `taskId` to match the rest of the SDK surface.
68
+ */
69
+ exports.WebhookItemCommentSchema = WebhookCommentBaseSchema.extend({
70
+ itemId: zod_1.z.string(),
71
+ url: zod_1.z.string(),
72
+ item: types_2.TaskSchema,
73
+ })
74
+ .strict()
75
+ .transform((_a) => {
76
+ var { itemId } = _a, rest = __rest(_a, ["itemId"]);
77
+ return (Object.assign(Object.assign({}, rest), { taskId: itemId }));
78
+ });
79
+ /**
80
+ * A comment attached to a project, delivered in a webhook payload.
81
+ *
82
+ * Project comments do not carry the extra `url` or embedded project fields
83
+ * that item comments carry for the parent task.
84
+ */
85
+ exports.WebhookProjectCommentSchema = WebhookCommentBaseSchema.extend({
86
+ projectId: zod_1.z.string(),
87
+ }).strict();
88
+ /**
89
+ * The `eventData` of a `note:*` webhook payload — either an item comment
90
+ * (includes `taskId` and an embedded {@link Task}) or a project comment
91
+ * (includes `projectId`). Narrow with `'taskId' in eventData`.
92
+ */
93
+ exports.WebhookCommentSchema = zod_1.z.union([exports.WebhookItemCommentSchema, exports.WebhookProjectCommentSchema]);
94
+ exports.CommentAddedPayloadSchema = zod_1.z.object(Object.assign(Object.assign({}, envelope_1.BaseWebhookEnvelopeShape), { eventName: zod_1.z.literal(exports.CommentWebhookEventEnum.Added), eventData: exports.WebhookCommentSchema }));
95
+ exports.CommentUpdatedPayloadSchema = zod_1.z.object(Object.assign(Object.assign({}, envelope_1.BaseWebhookEnvelopeShape), { eventName: zod_1.z.literal(exports.CommentWebhookEventEnum.Updated), eventData: exports.WebhookCommentSchema }));
96
+ exports.CommentDeletedPayloadSchema = zod_1.z.object(Object.assign(Object.assign({}, envelope_1.BaseWebhookEnvelopeShape), { eventName: zod_1.z.literal(exports.CommentWebhookEventEnum.Deleted), eventData: exports.WebhookCommentSchema }));
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseWebhookEnvelopeShape = exports.WebhookInitiatorSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const types_1 = require("../apps/types");
6
+ /**
7
+ * Collaborator info for the user that triggered a webhook event.
8
+ * @see https://developer.todoist.com/sync/v1/#webhooks
9
+ */
10
+ exports.WebhookInitiatorSchema = zod_1.z.object({
11
+ id: zod_1.z.string(),
12
+ fullName: zod_1.z.string(),
13
+ email: zod_1.z.string(),
14
+ imageId: zod_1.z.string().nullable(),
15
+ isPremium: zod_1.z.boolean(),
16
+ });
17
+ /**
18
+ * Fields common to every webhook payload, regardless of `eventName`.
19
+ * Meant to be spread into per-event variant schemas.
20
+ */
21
+ exports.BaseWebhookEnvelopeShape = {
22
+ version: types_1.WebhookVersionSchema,
23
+ userId: zod_1.z.string(),
24
+ initiator: exports.WebhookInitiatorSchema,
25
+ triggeredAt: zod_1.z.coerce.date(),
26
+ };
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./comments"), exports);
18
+ __exportStar(require("./envelope"), exports);
19
+ __exportStar(require("./labels"), exports);
20
+ __exportStar(require("./tasks"), exports);
21
+ __exportStar(require("./types"), exports);
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.LabelDeletedPayloadSchema = exports.LabelUpdatedPayloadSchema = exports.LabelAddedPayloadSchema = exports.WebhookLabelSchema = exports.LABEL_WEBHOOK_EVENTS = exports.LabelWebhookEventEnum = void 0;
15
+ const zod_1 = require("zod");
16
+ const types_1 = require("../labels/types");
17
+ const envelope_1 = require("./envelope");
18
+ /**
19
+ * Enum-like object of label (`label:*`) webhook event names. Single source
20
+ * of truth for the discriminator values — `LABEL_WEBHOOK_EVENTS` and every
21
+ * `z.literal()` below derive from it.
22
+ */
23
+ exports.LabelWebhookEventEnum = {
24
+ Added: 'label:added',
25
+ Updated: 'label:updated',
26
+ Deleted: 'label:deleted',
27
+ };
28
+ /** Webhook events that carry a label as their `eventData`. */
29
+ exports.LABEL_WEBHOOK_EVENTS = [
30
+ exports.LabelWebhookEventEnum.Added,
31
+ exports.LabelWebhookEventEnum.Updated,
32
+ exports.LabelWebhookEventEnum.Deleted,
33
+ ];
34
+ /**
35
+ * A label as delivered in a webhook payload.
36
+ *
37
+ * Webhook label payloads differ from the REST label shape in two ways:
38
+ * they use `item_order` (not `order`) and always include `is_deleted`. This
39
+ * schema reuses {@link LabelSchema}, swapping `order` for `itemOrder` on
40
+ * the input side and renaming it back after validation, and surfaces
41
+ * `isDeleted` for downstream filtering.
42
+ */
43
+ exports.WebhookLabelSchema = types_1.LabelSchema.omit({ order: true })
44
+ .extend({
45
+ itemOrder: zod_1.z.number().int().nullable(),
46
+ isDeleted: zod_1.z.boolean(),
47
+ })
48
+ .transform((_a) => {
49
+ var { itemOrder } = _a, rest = __rest(_a, ["itemOrder"]);
50
+ return (Object.assign(Object.assign({}, rest), { order: itemOrder }));
51
+ });
52
+ exports.LabelAddedPayloadSchema = zod_1.z.object(Object.assign(Object.assign({}, envelope_1.BaseWebhookEnvelopeShape), { eventName: zod_1.z.literal(exports.LabelWebhookEventEnum.Added), eventData: exports.WebhookLabelSchema }));
53
+ exports.LabelUpdatedPayloadSchema = zod_1.z.object(Object.assign(Object.assign({}, envelope_1.BaseWebhookEnvelopeShape), { eventName: zod_1.z.literal(exports.LabelWebhookEventEnum.Updated), eventData: exports.WebhookLabelSchema }));
54
+ exports.LabelDeletedPayloadSchema = zod_1.z.object(Object.assign(Object.assign({}, envelope_1.BaseWebhookEnvelopeShape), { eventName: zod_1.z.literal(exports.LabelWebhookEventEnum.Deleted), eventData: exports.WebhookLabelSchema }));
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TaskDeletedPayloadSchema = exports.TaskUncompletedPayloadSchema = exports.TaskCompletedPayloadSchema = exports.TaskUpdatedPayloadSchema = exports.TaskAddedPayloadSchema = exports.TaskUpdatedExtraSchema = exports.UpdateIntentSchema = exports.UPDATE_INTENTS = exports.TASK_WEBHOOK_EVENTS = exports.TaskWebhookEventEnum = void 0;
4
+ const zod_1 = require("zod");
5
+ const types_1 = require("../tasks/types");
6
+ const envelope_1 = require("./envelope");
7
+ /**
8
+ * Enum-like object of task (`item:*`) webhook event names. Single source of
9
+ * truth for the discriminator values — `TASK_WEBHOOK_EVENTS` and every
10
+ * `z.literal()` below derive from it.
11
+ */
12
+ exports.TaskWebhookEventEnum = {
13
+ Added: 'item:added',
14
+ Updated: 'item:updated',
15
+ Completed: 'item:completed',
16
+ Uncompleted: 'item:uncompleted',
17
+ Deleted: 'item:deleted',
18
+ };
19
+ /**
20
+ * Webhook events that carry a {@link Task} as their `eventData`.
21
+ *
22
+ * The on-the-wire event names remain `item:*` for historical reasons; the
23
+ * SDK types call them `Task*` to match the rest of the public API where the
24
+ * resource is named `Task`.
25
+ */
26
+ exports.TASK_WEBHOOK_EVENTS = [
27
+ exports.TaskWebhookEventEnum.Added,
28
+ exports.TaskWebhookEventEnum.Updated,
29
+ exports.TaskWebhookEventEnum.Completed,
30
+ exports.TaskWebhookEventEnum.Uncompleted,
31
+ exports.TaskWebhookEventEnum.Deleted,
32
+ ];
33
+ /**
34
+ * Possible values for the `updateIntent` field on `item:updated`'s
35
+ * `eventDataExtra`. `item_updated` is the default; the others narrow the
36
+ * reason for the update (for example, a recurring completion or a change of
37
+ * assignee).
38
+ *
39
+ * @see https://developer.todoist.com/sync/v1/#webhooks (Events Extra)
40
+ */
41
+ exports.UPDATE_INTENTS = [
42
+ 'item_updated',
43
+ 'item_completed',
44
+ 'item_uncompleted',
45
+ 'responsible_uid_changed',
46
+ ];
47
+ /**
48
+ * An unrecognised value from a future backend release degrades gracefully to
49
+ * `'item_updated'` rather than crashing the whole payload parse.
50
+ */
51
+ exports.UpdateIntentSchema = zod_1.z.enum(exports.UPDATE_INTENTS).catch('item_updated');
52
+ /**
53
+ * Extra info delivered alongside an `item:updated` event when the initiator
54
+ * is the acting user. Contains the pre-update task state plus the reason for
55
+ * the update — useful for diffing or for distinguishing manual edits from
56
+ * recurring-task completions.
57
+ *
58
+ * @see https://developer.todoist.com/sync/v1/#webhooks (Events Extra)
59
+ */
60
+ exports.TaskUpdatedExtraSchema = zod_1.z.object({
61
+ oldItem: types_1.TaskSchema,
62
+ updateIntent: exports.UpdateIntentSchema,
63
+ });
64
+ exports.TaskAddedPayloadSchema = zod_1.z.object(Object.assign(Object.assign({}, envelope_1.BaseWebhookEnvelopeShape), { eventName: zod_1.z.literal(exports.TaskWebhookEventEnum.Added), eventData: types_1.TaskSchema }));
65
+ exports.TaskUpdatedPayloadSchema = zod_1.z.object(Object.assign(Object.assign({}, envelope_1.BaseWebhookEnvelopeShape), { eventName: zod_1.z.literal(exports.TaskWebhookEventEnum.Updated), eventData: types_1.TaskSchema, eventDataExtra: exports.TaskUpdatedExtraSchema.optional() }));
66
+ exports.TaskCompletedPayloadSchema = zod_1.z.object(Object.assign(Object.assign({}, envelope_1.BaseWebhookEnvelopeShape), { eventName: zod_1.z.literal(exports.TaskWebhookEventEnum.Completed), eventData: types_1.TaskSchema }));
67
+ exports.TaskUncompletedPayloadSchema = zod_1.z.object(Object.assign(Object.assign({}, envelope_1.BaseWebhookEnvelopeShape), { eventName: zod_1.z.literal(exports.TaskWebhookEventEnum.Uncompleted), eventData: types_1.TaskSchema }));
68
+ exports.TaskDeletedPayloadSchema = zod_1.z.object(Object.assign(Object.assign({}, envelope_1.BaseWebhookEnvelopeShape), { eventName: zod_1.z.literal(exports.TaskWebhookEventEnum.Deleted), eventData: types_1.TaskSchema }));
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebhookEventSchema = exports.WebhookPayloadSchema = exports.UntypedWebhookPayloadSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const types_1 = require("../apps/types");
6
+ Object.defineProperty(exports, "WebhookEventSchema", { enumerable: true, get: function () { return types_1.WebhookEventSchema; } });
7
+ const comments_1 = require("./comments");
8
+ const envelope_1 = require("./envelope");
9
+ const labels_1 = require("./labels");
10
+ const tasks_1 = require("./tasks");
11
+ /**
12
+ * Events whose payloads have been narrowed per resource. Each phase adds to
13
+ * this list; {@link UntypedWebhookPayloadSchema} below is derived by taking
14
+ * every {@link WebhookEvent} not present here.
15
+ */
16
+ const TYPED_WEBHOOK_EVENTS = [
17
+ ...tasks_1.TASK_WEBHOOK_EVENTS,
18
+ ...comments_1.COMMENT_WEBHOOK_EVENTS,
19
+ ...labels_1.LABEL_WEBHOOK_EVENTS,
20
+ ];
21
+ const untypedEventNames = types_1.WEBHOOK_EVENTS.filter((event) => !TYPED_WEBHOOK_EVENTS.includes(event));
22
+ /**
23
+ * A single branch covering every event whose payload has not yet been typed
24
+ * per resource. Follow-up PRs will replace slices of this with dedicated
25
+ * variants (projects, sections, filters, reminders).
26
+ */
27
+ exports.UntypedWebhookPayloadSchema = zod_1.z.object(Object.assign(Object.assign({}, envelope_1.BaseWebhookEnvelopeShape), { eventName: zod_1.z.enum(untypedEventNames), eventData: zod_1.z.unknown(), eventDataExtra: zod_1.z.unknown().optional() }));
28
+ /**
29
+ * Wrapper schema for an incoming webhook payload.
30
+ *
31
+ * The backend sends snake_case keys and uses the public event form
32
+ * (`item:added`) in the `event_name` field, so consumers using
33
+ * `parseWebhookPayload` receive camelCase fields directly.
34
+ *
35
+ * Typed today: `item:*` events carry a parsed {@link Task}; `note:*` events
36
+ * carry a parsed comment (item-comment or project-comment); `label:*` events
37
+ * carry a parsed label. Other events still expose `eventData` as `unknown`;
38
+ * they will be narrowed in follow-up PRs (projects, sections, filters,
39
+ * reminders).
40
+ */
41
+ exports.WebhookPayloadSchema = zod_1.z.discriminatedUnion('eventName', [
42
+ tasks_1.TaskAddedPayloadSchema,
43
+ tasks_1.TaskUpdatedPayloadSchema,
44
+ tasks_1.TaskCompletedPayloadSchema,
45
+ tasks_1.TaskUncompletedPayloadSchema,
46
+ tasks_1.TaskDeletedPayloadSchema,
47
+ comments_1.CommentAddedPayloadSchema,
48
+ comments_1.CommentUpdatedPayloadSchema,
49
+ comments_1.CommentDeletedPayloadSchema,
50
+ labels_1.LabelAddedPayloadSchema,
51
+ labels_1.LabelUpdatedPayloadSchema,
52
+ labels_1.LabelDeletedPayloadSchema,
53
+ exports.UntypedWebhookPayloadSchema,
54
+ ]);
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.createCommand = exports.getSectionUrl = exports.getProjectUrl = exports.getTaskUrl = void 0;
17
+ exports.verifyWebhookSignature = exports.parseWebhookPayload = exports.createCommand = exports.getSectionUrl = exports.getProjectUrl = exports.getTaskUrl = void 0;
18
18
  __exportStar(require("./colors"), exports);
19
19
  __exportStar(require("./sanitization"), exports);
20
20
  var url_helpers_1 = require("./url-helpers");
@@ -24,3 +24,6 @@ Object.defineProperty(exports, "getSectionUrl", { enumerable: true, get: functio
24
24
  var sync_helpers_1 = require("./sync-helpers");
25
25
  Object.defineProperty(exports, "createCommand", { enumerable: true, get: function () { return sync_helpers_1.createCommand; } });
26
26
  __exportStar(require("./project-helpers"), exports);
27
+ var webhook_parser_1 = require("./webhook-parser");
28
+ Object.defineProperty(exports, "parseWebhookPayload", { enumerable: true, get: function () { return webhook_parser_1.parseWebhookPayload; } });
29
+ Object.defineProperty(exports, "verifyWebhookSignature", { enumerable: true, get: function () { return webhook_parser_1.verifyWebhookSignature; } });
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseWebhookPayload = parseWebhookPayload;
4
+ exports.verifyWebhookSignature = verifyWebhookSignature;
5
+ const types_1 = require("../types/webhooks/types");
6
+ const case_conversion_1 = require("./case-conversion");
7
+ /**
8
+ * Parses an incoming webhook request body into a typed payload.
9
+ *
10
+ * The backend sends snake_case keys (e.g. `event_name`, `user_id`, `added_at`);
11
+ * this converts them to camelCase and validates against
12
+ * {@link WebhookPayloadSchema}. Event-specific `eventData` is returned as
13
+ * `unknown` for events that are not yet narrowed per-resource.
14
+ *
15
+ * Pass the parsed JSON body (or any unknown value) — validation throws on
16
+ * unexpected shape.
17
+ */
18
+ function parseWebhookPayload(raw) {
19
+ return types_1.WebhookPayloadSchema.parse((0, case_conversion_1.camelCaseKeys)(raw));
20
+ }
21
+ const encoder = new TextEncoder();
22
+ function base64ToBytes(base64) {
23
+ const binary = atob(base64);
24
+ const bytes = new Uint8Array(binary.length);
25
+ for (let i = 0; i < binary.length; i++) {
26
+ bytes[i] = binary.charCodeAt(i);
27
+ }
28
+ return bytes;
29
+ }
30
+ /**
31
+ * Verifies a webhook request's HMAC signature.
32
+ *
33
+ * Todoist signs webhook deliveries with an HMAC-SHA256 of the raw request body
34
+ * using the app's `client_secret` as the key, base64-encoded in the
35
+ * `X-Todoist-Hmac-SHA256` header.
36
+ *
37
+ * Uses the Web Crypto API (`crypto.subtle`), which is available in Node ≥ 20,
38
+ * modern browsers, React Native (with polyfills), and edge runtimes. The
39
+ * comparison is constant-time.
40
+ *
41
+ * @returns `true` when the signature is valid, `false` otherwise. Never throws
42
+ * on malformed input.
43
+ */
44
+ async function verifyWebhookSignature({ rawBody, signature, clientSecret, }) {
45
+ let provided;
46
+ try {
47
+ provided = base64ToBytes(signature);
48
+ }
49
+ catch (_a) {
50
+ return false;
51
+ }
52
+ const key = await crypto.subtle.importKey('raw', encoder.encode(clientSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
53
+ const body = typeof rawBody === 'string' ? encoder.encode(rawBody) : rawBody;
54
+ try {
55
+ return await crypto.subtle.verify('HMAC', key, provided, body);
56
+ }
57
+ catch (_b) {
58
+ return false;
59
+ }
60
+ }
@@ -0,0 +1,26 @@
1
+ import { createHmac } from 'node:crypto';
2
+ export const CLIENT_SECRET = 'test-client-secret';
3
+ /** HMAC-SHA256 signs a raw body with the test client secret (or an override). */
4
+ export function sign(body, secret = CLIENT_SECRET) {
5
+ return createHmac('sha256', secret).update(body).digest('base64');
6
+ }
7
+ /**
8
+ * Snake_case task payload matching the v1 webhook wire shape.
9
+ */
10
+ export function rawTask(overrides = {}) {
11
+ return Object.assign({ id: '6XR4GqQQCW6Gv9h4', user_id: '2671355', project_id: '6XR4H993xv8H5qCR', section_id: null, parent_id: null, added_by_uid: '2671355', assigned_by_uid: null, responsible_uid: null, labels: [], deadline: null, duration: null, is_collapsed: false, checked: false, is_deleted: false, added_at: '2025-02-10T10:33:38.000000Z', completed_at: null, completed_by_uid: null, updated_at: '2025-02-10T10:33:38.000000Z', due: null, priority: 1, child_order: 3, content: 'Buy Milk', description: '', note_count: 0, day_order: -1, goal_ids: [], url: 'https://app.todoist.com/app/task/6XR4GqQQCW6Gv9h4' }, overrides);
12
+ }
13
+ /**
14
+ * Wraps an event's `event_data` in the snake_case envelope the backend sends
15
+ * (initiator, version, timestamps, etc). `extra` passes extra top-level keys
16
+ * like `event_data_extra`.
17
+ */
18
+ export function envelope(event, eventData, extra) {
19
+ return Object.assign({ version: '1', user_id: '2671355', initiator: {
20
+ id: '2671355',
21
+ full_name: 'Alice',
22
+ email: 'alice@example.com',
23
+ image_id: 'ad38375bdb094286af59f1eab36d8f20',
24
+ is_premium: true,
25
+ }, event_name: event, event_data: eventData, triggered_at: '2025-02-10T10:39:38.000000Z' }, extra);
26
+ }
@@ -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,93 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { z } from 'zod';
13
+ import { AttachmentSchema } from '../comments/types.js';
14
+ import { TaskSchema } from '../tasks/types.js';
15
+ import { BaseWebhookEnvelopeShape } from './envelope.js';
16
+ /**
17
+ * Enum-like object of comment (`note:*`) webhook event names. Single source
18
+ * of truth for the discriminator values — `COMMENT_WEBHOOK_EVENTS` and every
19
+ * `z.literal()` below derive from it.
20
+ *
21
+ * The on-the-wire event names remain `note:*` for historical reasons; the
22
+ * SDK types use `Comment*` to match the rest of the public API where the
23
+ * resource is named `Comment`.
24
+ */
25
+ export const CommentWebhookEventEnum = {
26
+ Added: 'note:added',
27
+ Updated: 'note:updated',
28
+ Deleted: 'note:deleted',
29
+ };
30
+ /**
31
+ * Webhook events that carry a comment as their `eventData`.
32
+ */
33
+ export const COMMENT_WEBHOOK_EVENTS = [
34
+ CommentWebhookEventEnum.Added,
35
+ CommentWebhookEventEnum.Updated,
36
+ CommentWebhookEventEnum.Deleted,
37
+ ];
38
+ /**
39
+ * Fields common to both item and project comment payloads.
40
+ *
41
+ * `postedAt` and `postedUid` are nullable here (unlike
42
+ * {@link RawCommentSchema}) because webhook payloads may omit them — for
43
+ * example when the poster could not be resolved, or on a deleted-note
44
+ * tombstone.
45
+ */
46
+ const WebhookCommentBaseSchema = z.object({
47
+ id: z.string(),
48
+ content: z.string(),
49
+ postedAt: z.coerce.date().nullable(),
50
+ postedUid: z.string().nullable(),
51
+ fileAttachment: AttachmentSchema.nullable(),
52
+ uidsToNotify: z.array(z.string()).nullable(),
53
+ reactions: z.record(z.string(), z.array(z.string())).nullable(),
54
+ isDeleted: z.boolean(),
55
+ });
56
+ /**
57
+ * A comment attached to a task, delivered in a webhook payload.
58
+ *
59
+ * Webhook item-comment payloads carry two fields that don't appear on the
60
+ * REST comment resource: `url` (the direct task URL) and `item` (the full
61
+ * embedded {@link Task}). Both are parsed here so consumers handling a
62
+ * `note:added` event also get the task it belongs to.
63
+ *
64
+ * `itemId` is exposed as `taskId` to match the rest of the SDK surface.
65
+ */
66
+ export const WebhookItemCommentSchema = WebhookCommentBaseSchema.extend({
67
+ itemId: z.string(),
68
+ url: z.string(),
69
+ item: TaskSchema,
70
+ })
71
+ .strict()
72
+ .transform((_a) => {
73
+ var { itemId } = _a, rest = __rest(_a, ["itemId"]);
74
+ return (Object.assign(Object.assign({}, rest), { taskId: itemId }));
75
+ });
76
+ /**
77
+ * A comment attached to a project, delivered in a webhook payload.
78
+ *
79
+ * Project comments do not carry the extra `url` or embedded project fields
80
+ * that item comments carry for the parent task.
81
+ */
82
+ export const WebhookProjectCommentSchema = WebhookCommentBaseSchema.extend({
83
+ projectId: z.string(),
84
+ }).strict();
85
+ /**
86
+ * The `eventData` of a `note:*` webhook payload — either an item comment
87
+ * (includes `taskId` and an embedded {@link Task}) or a project comment
88
+ * (includes `projectId`). Narrow with `'taskId' in eventData`.
89
+ */
90
+ export const WebhookCommentSchema = z.union([WebhookItemCommentSchema, WebhookProjectCommentSchema]);
91
+ export const CommentAddedPayloadSchema = z.object(Object.assign(Object.assign({}, BaseWebhookEnvelopeShape), { eventName: z.literal(CommentWebhookEventEnum.Added), eventData: WebhookCommentSchema }));
92
+ export const CommentUpdatedPayloadSchema = z.object(Object.assign(Object.assign({}, BaseWebhookEnvelopeShape), { eventName: z.literal(CommentWebhookEventEnum.Updated), eventData: WebhookCommentSchema }));
93
+ export const CommentDeletedPayloadSchema = z.object(Object.assign(Object.assign({}, BaseWebhookEnvelopeShape), { eventName: z.literal(CommentWebhookEventEnum.Deleted), eventData: WebhookCommentSchema }));
@@ -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,5 @@
1
+ export * from './comments.js';
2
+ export * from './envelope.js';
3
+ export * from './labels.js';
4
+ export * from './tasks.js';
5
+ export * from './types.js';
@@ -0,0 +1,51 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { z } from 'zod';
13
+ import { LabelSchema } from '../labels/types.js';
14
+ import { BaseWebhookEnvelopeShape } from './envelope.js';
15
+ /**
16
+ * Enum-like object of label (`label:*`) webhook event names. Single source
17
+ * of truth for the discriminator values — `LABEL_WEBHOOK_EVENTS` and every
18
+ * `z.literal()` below derive from it.
19
+ */
20
+ export const LabelWebhookEventEnum = {
21
+ Added: 'label:added',
22
+ Updated: 'label:updated',
23
+ Deleted: 'label:deleted',
24
+ };
25
+ /** Webhook events that carry a label as their `eventData`. */
26
+ export const LABEL_WEBHOOK_EVENTS = [
27
+ LabelWebhookEventEnum.Added,
28
+ LabelWebhookEventEnum.Updated,
29
+ LabelWebhookEventEnum.Deleted,
30
+ ];
31
+ /**
32
+ * A label as delivered in a webhook payload.
33
+ *
34
+ * Webhook label payloads differ from the REST label shape in two ways:
35
+ * they use `item_order` (not `order`) and always include `is_deleted`. This
36
+ * schema reuses {@link LabelSchema}, swapping `order` for `itemOrder` on
37
+ * the input side and renaming it back after validation, and surfaces
38
+ * `isDeleted` for downstream filtering.
39
+ */
40
+ export const WebhookLabelSchema = LabelSchema.omit({ order: true })
41
+ .extend({
42
+ itemOrder: z.number().int().nullable(),
43
+ isDeleted: z.boolean(),
44
+ })
45
+ .transform((_a) => {
46
+ var { itemOrder } = _a, rest = __rest(_a, ["itemOrder"]);
47
+ return (Object.assign(Object.assign({}, rest), { order: itemOrder }));
48
+ });
49
+ export const LabelAddedPayloadSchema = z.object(Object.assign(Object.assign({}, BaseWebhookEnvelopeShape), { eventName: z.literal(LabelWebhookEventEnum.Added), eventData: WebhookLabelSchema }));
50
+ export const LabelUpdatedPayloadSchema = z.object(Object.assign(Object.assign({}, BaseWebhookEnvelopeShape), { eventName: z.literal(LabelWebhookEventEnum.Updated), eventData: WebhookLabelSchema }));
51
+ export const LabelDeletedPayloadSchema = z.object(Object.assign(Object.assign({}, BaseWebhookEnvelopeShape), { eventName: z.literal(LabelWebhookEventEnum.Deleted), eventData: WebhookLabelSchema }));