@doist/todoist-sdk 9.3.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.
- package/dist/cjs/types/index.js +1 -0
- package/dist/cjs/types/webhooks/envelope.js +26 -0
- package/dist/cjs/types/webhooks/index.js +19 -0
- package/dist/cjs/types/webhooks/tasks.js +68 -0
- package/dist/cjs/types/webhooks/types.js +34 -0
- package/dist/cjs/utils/index.js +4 -1
- package/dist/cjs/utils/webhook-parser.js +60 -0
- package/dist/esm/types/index.js +1 -0
- package/dist/esm/types/webhooks/envelope.js +23 -0
- package/dist/esm/types/webhooks/index.js +3 -0
- package/dist/esm/types/webhooks/tasks.js +65 -0
- package/dist/esm/types/webhooks/types.js +31 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/esm/utils/webhook-parser.js +56 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/webhooks/envelope.d.ts +34 -0
- package/dist/types/types/webhooks/index.d.ts +3 -0
- package/dist/types/types/webhooks/tasks.d.ts +981 -0
- package/dist/types/types/webhooks/types.d.ts +893 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/dist/types/utils/webhook-parser.d.ts +48 -0
- package/package.json +1 -1
package/dist/cjs/types/index.js
CHANGED
|
@@ -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,19 @@
|
|
|
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("./envelope"), exports);
|
|
18
|
+
__exportStar(require("./tasks"), exports);
|
|
19
|
+
__exportStar(require("./types"), exports);
|
|
@@ -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,34 @@
|
|
|
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 envelope_1 = require("./envelope");
|
|
8
|
+
const tasks_1 = require("./tasks");
|
|
9
|
+
const untypedEventNames = types_1.WEBHOOK_EVENTS.filter((event) => !tasks_1.TASK_WEBHOOK_EVENTS.includes(event));
|
|
10
|
+
/**
|
|
11
|
+
* A single branch covering every event whose payload has not yet been typed
|
|
12
|
+
* per resource. Follow-up PRs will replace slices of this with dedicated
|
|
13
|
+
* variants (notes, projects, sections, labels, filters, reminders).
|
|
14
|
+
*/
|
|
15
|
+
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() }));
|
|
16
|
+
/**
|
|
17
|
+
* Wrapper schema for an incoming webhook payload.
|
|
18
|
+
*
|
|
19
|
+
* The backend sends snake_case keys and uses the public event form
|
|
20
|
+
* (`item:added`) in the `event_name` field, so consumers using
|
|
21
|
+
* `parseWebhookPayload` receive camelCase fields directly.
|
|
22
|
+
*
|
|
23
|
+
* Typed today: `item:*` events carry a parsed {@link Task}. Other events
|
|
24
|
+
* still expose `eventData` as `unknown`; they will be narrowed in follow-up
|
|
25
|
+
* PRs (notes, projects, sections, labels, filters, reminders).
|
|
26
|
+
*/
|
|
27
|
+
exports.WebhookPayloadSchema = zod_1.z.discriminatedUnion('eventName', [
|
|
28
|
+
tasks_1.TaskAddedPayloadSchema,
|
|
29
|
+
tasks_1.TaskUpdatedPayloadSchema,
|
|
30
|
+
tasks_1.TaskCompletedPayloadSchema,
|
|
31
|
+
tasks_1.TaskUncompletedPayloadSchema,
|
|
32
|
+
tasks_1.TaskDeletedPayloadSchema,
|
|
33
|
+
exports.UntypedWebhookPayloadSchema,
|
|
34
|
+
]);
|
package/dist/cjs/utils/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/esm/types/index.js
CHANGED
|
@@ -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,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 };
|
package/dist/esm/utils/index.js
CHANGED
|
@@ -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';
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
};
|