@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,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,51 @@
1
+ import { z } from 'zod';
2
+ import { WEBHOOK_EVENTS, WebhookEventSchema } from '../apps/types.js';
3
+ import { COMMENT_WEBHOOK_EVENTS, CommentAddedPayloadSchema, CommentDeletedPayloadSchema, CommentUpdatedPayloadSchema, } from './comments.js';
4
+ import { BaseWebhookEnvelopeShape } from './envelope.js';
5
+ import { LABEL_WEBHOOK_EVENTS, LabelAddedPayloadSchema, LabelDeletedPayloadSchema, LabelUpdatedPayloadSchema, } from './labels.js';
6
+ import { TASK_WEBHOOK_EVENTS, TaskAddedPayloadSchema, TaskCompletedPayloadSchema, TaskDeletedPayloadSchema, TaskUncompletedPayloadSchema, TaskUpdatedPayloadSchema, } from './tasks.js';
7
+ /**
8
+ * Events whose payloads have been narrowed per resource. Each phase adds to
9
+ * this list; {@link UntypedWebhookPayloadSchema} below is derived by taking
10
+ * every {@link WebhookEvent} not present here.
11
+ */
12
+ const TYPED_WEBHOOK_EVENTS = [
13
+ ...TASK_WEBHOOK_EVENTS,
14
+ ...COMMENT_WEBHOOK_EVENTS,
15
+ ...LABEL_WEBHOOK_EVENTS,
16
+ ];
17
+ const untypedEventNames = WEBHOOK_EVENTS.filter((event) => !TYPED_WEBHOOK_EVENTS.includes(event));
18
+ /**
19
+ * A single branch covering every event whose payload has not yet been typed
20
+ * per resource. Follow-up PRs will replace slices of this with dedicated
21
+ * variants (projects, sections, filters, reminders).
22
+ */
23
+ export const UntypedWebhookPayloadSchema = z.object(Object.assign(Object.assign({}, BaseWebhookEnvelopeShape), { eventName: z.enum(untypedEventNames), eventData: z.unknown(), eventDataExtra: z.unknown().optional() }));
24
+ /**
25
+ * Wrapper schema for an incoming webhook payload.
26
+ *
27
+ * The backend sends snake_case keys and uses the public event form
28
+ * (`item:added`) in the `event_name` field, so consumers using
29
+ * `parseWebhookPayload` receive camelCase fields directly.
30
+ *
31
+ * Typed today: `item:*` events carry a parsed {@link Task}; `note:*` events
32
+ * carry a parsed comment (item-comment or project-comment); `label:*` events
33
+ * carry a parsed label. Other events still expose `eventData` as `unknown`;
34
+ * they will be narrowed in follow-up PRs (projects, sections, filters,
35
+ * reminders).
36
+ */
37
+ export const WebhookPayloadSchema = z.discriminatedUnion('eventName', [
38
+ TaskAddedPayloadSchema,
39
+ TaskUpdatedPayloadSchema,
40
+ TaskCompletedPayloadSchema,
41
+ TaskUncompletedPayloadSchema,
42
+ TaskDeletedPayloadSchema,
43
+ CommentAddedPayloadSchema,
44
+ CommentUpdatedPayloadSchema,
45
+ CommentDeletedPayloadSchema,
46
+ LabelAddedPayloadSchema,
47
+ LabelUpdatedPayloadSchema,
48
+ LabelDeletedPayloadSchema,
49
+ UntypedWebhookPayloadSchema,
50
+ ]);
51
+ 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';
@@ -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,13 @@
1
+ export declare const CLIENT_SECRET = "test-client-secret";
2
+ /** HMAC-SHA256 signs a raw body with the test client secret (or an override). */
3
+ export declare function sign(body: string, secret?: string): string;
4
+ /**
5
+ * Snake_case task payload matching the v1 webhook wire shape.
6
+ */
7
+ export declare function rawTask(overrides?: Record<string, unknown>): Record<string, unknown>;
8
+ /**
9
+ * Wraps an event's `event_data` in the snake_case envelope the backend sends
10
+ * (initiator, version, timestamps, etc). `extra` passes extra top-level keys
11
+ * like `event_data_extra`.
12
+ */
13
+ export declare function envelope(event: string, eventData: unknown, extra?: Record<string, unknown>): Record<string, unknown>;
@@ -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';