@todoist/automations-sdk 0.0.0 → 1.0.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.
@@ -0,0 +1,34 @@
1
+ //#region src/errors/request-error.ts
2
+ var AutomationsNetworkError = class extends Error {
3
+ constructor(message, cause) {
4
+ super(message, { cause });
5
+ this.name = "AutomationsNetworkError";
6
+ }
7
+ };
8
+ var AutomationsRequestError = class extends Error {
9
+ httpStatusCode;
10
+ responseData;
11
+ authChallenge;
12
+ requiredScope;
13
+ requestId;
14
+ constructor(message, httpStatusCode, responseData, authChallenge, requestId) {
15
+ super(message);
16
+ this.name = "AutomationsRequestError";
17
+ this.httpStatusCode = httpStatusCode;
18
+ this.responseData = responseData;
19
+ this.authChallenge = authChallenge;
20
+ this.requiredScope = parseChallengeScope(authChallenge);
21
+ this.requestId = requestId;
22
+ }
23
+ isAuthError() {
24
+ return this.httpStatusCode === 401 || this.httpStatusCode === 403;
25
+ }
26
+ };
27
+ function parseChallengeScope(header) {
28
+ return header?.match(/scope="([^"]+)"/)?.[1];
29
+ }
30
+ function isInsufficientScopeChallenge(header) {
31
+ return header?.includes("error=\"insufficient_scope\"") ?? false;
32
+ }
33
+ //#endregion
34
+ export { AutomationsRequestError as n, isInsufficientScopeChallenge as r, AutomationsNetworkError as t };
@@ -0,0 +1,31 @@
1
+ //#region ../core/dist/esm/constants/webhook-event-subscription.constants.js
2
+ const WEBHOOK_EVENT_TYPES = [
3
+ "automation:run:started",
4
+ "automation:run:finished",
5
+ "automation:created",
6
+ "automation:updated",
7
+ "automation:deleted",
8
+ "automation:enabled",
9
+ "automation:disabled",
10
+ "connection:broken"
11
+ ];
12
+ const ACCOUNT_SCOPED_WEBHOOK_EVENT_TYPES = ["connection:broken"];
13
+ function getAccountScopedEventTypes(eventTypes) {
14
+ const accountScoped = new Set(ACCOUNT_SCOPED_WEBHOOK_EVENT_TYPES);
15
+ return eventTypes.filter((type) => accountScoped.has(type));
16
+ }
17
+ const WEBHOOK_EVENT_SUBSCRIPTION_STATUSES = ["active", "disabled"];
18
+ const WEBHOOK_EVENT_SUBSCRIPTION_DISABLED_REASONS = ["consecutive_failures"];
19
+ const WEBHOOK_EVENT_DELIVERY_STATUSES = [
20
+ "pending",
21
+ "succeeded",
22
+ "failed",
23
+ "exhausted",
24
+ "aborted"
25
+ ];
26
+ const WEBHOOK_SECRET_PREFIX = "whsec_";
27
+ const WEBHOOK_ID_HEADER = "webhook-id";
28
+ const WEBHOOK_TIMESTAMP_HEADER = "webhook-timestamp";
29
+ const WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
30
+ //#endregion
31
+ export { WEBHOOK_ID_HEADER as a, WEBHOOK_TIMESTAMP_HEADER as c, WEBHOOK_EVENT_TYPES as i, getAccountScopedEventTypes as l, WEBHOOK_EVENT_SUBSCRIPTION_DISABLED_REASONS as n, WEBHOOK_SECRET_PREFIX as o, WEBHOOK_EVENT_SUBSCRIPTION_STATUSES as r, WEBHOOK_SIGNATURE_HEADER as s, WEBHOOK_EVENT_DELIVERY_STATUSES as t };
@@ -0,0 +1,52 @@
1
+ //#region ../core/dist/types/webhooks/signing.d.ts
2
+ type WebhookVerificationResult = {
3
+ valid: true;
4
+ } | {
5
+ valid: false;
6
+ reason: 'timestamp_invalid' | 'timestamp_expired' | 'signature_mismatch';
7
+ };
8
+ /**
9
+ * Verify an inbound delivery the way a receiver should: reject stale timestamps,
10
+ * recompute the expected signature, and constant-time compare against every
11
+ * entry in the header (any match passes — supports multi-sig rotation).
12
+ */
13
+ export declare function verifyWebhook(params: {
14
+ /**
15
+ * The signing secret, or several to support dual-key overlap during secret
16
+ * rotation — the delivery validates if it matches any of them.
17
+ */
18
+ secret: string | readonly string[];
19
+ webhookId: string;
20
+ timestamp: number | string;
21
+ signatureHeader: string;
22
+ rawBody: string;
23
+ /** Unix seconds; defaults to now. Injectable for deterministic tests. */
24
+ nowSeconds?: number;
25
+ toleranceSeconds?: number;
26
+ }): WebhookVerificationResult;
27
+ //#endregion
28
+ //#region src/webhooks/verify.d.ts
29
+ export type HeaderSource = Headers | Record<string, string | string[] | undefined>;
30
+ export type VerifyWebhookRequestParams = {
31
+ /**
32
+ * The subscription's signing secret (`whsec_…`), or several to support
33
+ * dual-key overlap during secret rotation — any match passes.
34
+ */
35
+ secret: string | readonly string[];
36
+ /** Inbound request headers — a `Headers` instance or a plain header map. */
37
+ headers: HeaderSource;
38
+ /** The exact raw request body bytes, as received (do not re-serialize). */
39
+ rawBody: string;
40
+ /** Unix seconds; defaults to now. Injectable for deterministic tests. */
41
+ nowSeconds?: number;
42
+ toleranceSeconds?: number;
43
+ };
44
+ /**
45
+ * Verify an inbound Standard Webhooks delivery from request headers + raw body.
46
+ * Pulls the `webhook-id` / `webhook-timestamp` / `webhook-signature` headers and
47
+ * delegates to the shared signing helper. A missing required header is reported
48
+ * as a signature mismatch.
49
+ */
50
+ export declare function verifyWebhookRequest(params: VerifyWebhookRequestParams): WebhookVerificationResult;
51
+ //#endregion
52
+ export type { WebhookVerificationResult };
@@ -0,0 +1,83 @@
1
+ import { a as WEBHOOK_ID_HEADER, c as WEBHOOK_TIMESTAMP_HEADER, o as WEBHOOK_SECRET_PREFIX, s as WEBHOOK_SIGNATURE_HEADER } from "./webhook-event-subscription.constants-CBmz0huQ.js";
2
+ import { createHmac, timingSafeEqual } from "node:crypto";
3
+ //#region ../core/dist/esm/webhooks/signing.js
4
+ const SIGNATURE_VERSION = "v1";
5
+ function decodeWebhookSecret(secret) {
6
+ const base64 = secret.startsWith("whsec_") ? secret.slice(WEBHOOK_SECRET_PREFIX.length) : secret;
7
+ return Buffer.from(base64, "base64");
8
+ }
9
+ function computeWebhookSignature(params) {
10
+ const { secret, webhookId, timestamp, rawBody } = params;
11
+ return createHmac("sha256", decodeWebhookSecret(secret)).update(`${webhookId}.${timestamp}.`, "utf8").update(rawBody, "utf8").digest("base64");
12
+ }
13
+ function constantTimeEquals(a, b) {
14
+ const aBuf = Buffer.from(a, "utf8");
15
+ const bBuf = Buffer.from(b, "utf8");
16
+ if (aBuf.length !== bBuf.length) return false;
17
+ return timingSafeEqual(aBuf, bBuf);
18
+ }
19
+ function verifyWebhook(params) {
20
+ const { secret, webhookId, timestamp, signatureHeader, rawBody, nowSeconds = Math.floor(Date.now() / 1e3), toleranceSeconds = 300 } = params;
21
+ const timestampSeconds = typeof timestamp === "string" ? Number(timestamp) : timestamp;
22
+ if (!Number.isFinite(timestampSeconds)) return {
23
+ valid: false,
24
+ reason: "timestamp_invalid"
25
+ };
26
+ if (Math.abs(nowSeconds - timestampSeconds) > toleranceSeconds) return {
27
+ valid: false,
28
+ reason: "timestamp_expired"
29
+ };
30
+ const secrets = typeof secret === "string" ? [secret] : secret;
31
+ let expectedSignatures;
32
+ return signatureHeader.split(" ").map((entry) => entry.split(",", 2)).filter(([version]) => version === SIGNATURE_VERSION).some(([, candidate]) => {
33
+ if (!candidate) return false;
34
+ expectedSignatures ??= secrets.map((candidateSecret) => computeWebhookSignature({
35
+ secret: candidateSecret,
36
+ webhookId,
37
+ timestamp,
38
+ rawBody
39
+ }));
40
+ return expectedSignatures.some((expected) => constantTimeEquals(candidate, expected));
41
+ }) ? { valid: true } : {
42
+ valid: false,
43
+ reason: "signature_mismatch"
44
+ };
45
+ }
46
+ //#endregion
47
+ //#region src/webhooks/verify.ts
48
+ function readWebhookHeaders(headers) {
49
+ if (headers instanceof Headers) return {
50
+ webhookId: headers.get(WEBHOOK_ID_HEADER),
51
+ timestamp: headers.get(WEBHOOK_TIMESTAMP_HEADER),
52
+ signatureHeader: headers.get(WEBHOOK_SIGNATURE_HEADER)
53
+ };
54
+ const normalized = {};
55
+ for (const [key, value] of Object.entries(headers)) {
56
+ if (value === void 0) continue;
57
+ normalized[key.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
58
+ }
59
+ return {
60
+ webhookId: normalized["webhook-id"] ?? null,
61
+ timestamp: normalized["webhook-timestamp"] ?? null,
62
+ signatureHeader: normalized["webhook-signature"] ?? null
63
+ };
64
+ }
65
+ function verifyWebhookRequest(params) {
66
+ const { secret, headers, rawBody, nowSeconds, toleranceSeconds } = params;
67
+ const { webhookId, timestamp, signatureHeader } = readWebhookHeaders(headers);
68
+ if (!webhookId || !timestamp || !signatureHeader) return {
69
+ valid: false,
70
+ reason: "signature_mismatch"
71
+ };
72
+ return verifyWebhook({
73
+ secret,
74
+ webhookId,
75
+ timestamp,
76
+ signatureHeader,
77
+ rawBody,
78
+ nowSeconds,
79
+ toleranceSeconds
80
+ });
81
+ }
82
+ //#endregion
83
+ export { verifyWebhook, verifyWebhookRequest };
package/package.json CHANGED
@@ -1,10 +1,85 @@
1
1
  {
2
- "name": "@todoist/automations-sdk",
3
- "version": "0.0.0",
4
- "description": "OIDC trusted publishing setup package for @todoist/automations-sdk",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
2
+ "name": "@todoist/automations-sdk",
3
+ "version": "1.0.0",
4
+ "description": "TypeScript SDK for the Todoist Automations API",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/todoist/automations.git",
9
+ "directory": "packages/sdk"
10
+ },
11
+ "homepage": "https://github.com/todoist/automations/tree/main/packages/sdk#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/todoist/automations/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "type": "module",
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./base-client": {
27
+ "types": "./dist/base-client.d.ts",
28
+ "default": "./dist/base-client.js"
29
+ },
30
+ "./webhooks": {
31
+ "types": "./dist/webhooks.d.ts",
32
+ "default": "./dist/webhooks.js"
33
+ },
34
+ "./chat-turn": {
35
+ "types": "./dist/chat-turn.d.ts",
36
+ "default": "./dist/chat-turn.js"
37
+ },
38
+ "./authoring": {
39
+ "types": "./dist/authoring.d.ts",
40
+ "default": "./dist/authoring.js"
41
+ }
42
+ },
43
+ "files": [
44
+ "dist"
45
+ ],
46
+ "sideEffects": false,
47
+ "scripts": {
48
+ "build": "tsdown",
49
+ "prepack": "npm run build",
50
+ "check:publish": "publint . && attw --pack . --profile esm-only && node scripts/check-no-admin.mjs",
51
+ "release": "semantic-release",
52
+ "test": "vitest run",
53
+ "lint": "oxlint src",
54
+ "lint:fix": "oxlint src --fix",
55
+ "format": "oxfmt src --check",
56
+ "format:fix": "oxfmt src",
57
+ "check": "oxfmt src --check && oxlint src",
58
+ "check:fix": "oxfmt src && oxlint src --fix",
59
+ "type-check": "tsc -p tsconfig.typecheck.json --noEmit"
60
+ },
61
+ "dependencies": {
62
+ "@types/json-schema": "7.0.15",
63
+ "ai": "7.0.163",
64
+ "json-stable-stringify": "1.3.0"
65
+ },
66
+ "peerDependencies": {
67
+ "zod": "^4.6.0"
68
+ },
69
+ "devDependencies": {
70
+ "@arethetypeswrong/cli": "0.18.5",
71
+ "@todoist/automations-core": "*",
72
+ "@todoist/automations-sdk-test-utils": "*",
73
+ "@todoist/tsconfig": "2.0.2",
74
+ "conventional-changelog-conventionalcommits": "9.3.1",
75
+ "msw": "3.0.3",
76
+ "publint": "0.3.25",
77
+ "semantic-release": "25.0.9",
78
+ "tsdown": "0.23.1",
79
+ "typescript": "6.0.3",
80
+ "zod": "4.6.7"
81
+ },
82
+ "engines": {
83
+ "node": ">=24"
84
+ }
10
85
  }