@doist/todoist-sdk 15.5.0 → 15.7.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 (36) hide show
  1. package/README.md +22 -0
  2. package/dist/esm/clients/activity-client.js +1 -0
  3. package/dist/esm/clients/app-client.js +25 -0
  4. package/dist/esm/clients/backup-client.js +5 -1
  5. package/dist/esm/clients/base-client.js +3 -1
  6. package/dist/esm/clients/billing-client.js +12 -0
  7. package/dist/esm/clients/comment-client.js +5 -0
  8. package/dist/esm/clients/email-client.js +2 -0
  9. package/dist/esm/clients/folder-client.js +5 -0
  10. package/dist/esm/clients/id-mapping-client.js +2 -0
  11. package/dist/esm/clients/insights-client.js +6 -0
  12. package/dist/esm/clients/label-client.js +9 -0
  13. package/dist/esm/clients/productivity-client.js +1 -0
  14. package/dist/esm/clients/project-client.js +16 -0
  15. package/dist/esm/clients/reminder-client.js +10 -0
  16. package/dist/esm/clients/section-client.js +8 -0
  17. package/dist/esm/clients/task-client.js +14 -0
  18. package/dist/esm/clients/template-client.js +5 -0
  19. package/dist/esm/clients/ui-extension-client.js +7 -0
  20. package/dist/esm/clients/upload-client.js +2 -0
  21. package/dist/esm/clients/workspace-client.js +22 -0
  22. package/dist/esm/todoist-api.js +4 -0
  23. package/dist/esm/transport/http-client.js +3 -1
  24. package/dist/esm/transport/sync-request.js +2 -1
  25. package/dist/esm/utils/colors.js +1 -2
  26. package/dist/esm/utils/headers.js +29 -0
  27. package/dist/esm/utils/multipart-upload.js +7 -10
  28. package/dist/types/clients/base-client.d.ts +3 -2
  29. package/dist/types/todoist-api.d.ts +9 -1
  30. package/dist/types/transport/http-client.d.ts +2 -2
  31. package/dist/types/transport/sync-request.d.ts +3 -2
  32. package/dist/types/types/http.d.ts +6 -0
  33. package/dist/types/utils/colors.d.ts +1 -2
  34. package/dist/types/utils/headers.d.ts +13 -0
  35. package/dist/types/utils/multipart-upload.d.ts +3 -1
  36. package/package.json +2 -2
@@ -3,6 +3,7 @@ import { API_BASE_URI } from '../consts/endpoints.js';
3
3
  import { TodoistRequestError } from '../types/errors.js';
4
4
  import { isHttpError, isNetworkError } from '../types/http.js';
5
5
  import { camelCaseKeys, snakeCaseKeys } from '../utils/case-conversion.js';
6
+ import { mergeHeaders, resolveCustomHeaders } from '../utils/headers.js';
6
7
  import { fetchWithRetry } from './fetch-with-retry.js';
7
8
  export function paramsSerializer(params) {
8
9
  const qs = new URLSearchParams();
@@ -59,7 +60,8 @@ function getRequestConfiguration(args) {
59
60
  const { baseURL, apiToken, requestId, customHeaders } = args;
60
61
  const authHeader = apiToken ? { Authorization: getAuthHeader(apiToken) } : undefined;
61
62
  const requestIdHeader = requestId ? { 'X-Request-Id': requestId } : undefined;
62
- const headers = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultHeaders), authHeader), requestIdHeader), customHeaders);
63
+ // Custom headers sit below auth and request id so they can never replace them.
64
+ const headers = mergeHeaders(defaultHeaders, resolveCustomHeaders(customHeaders), authHeader, requestIdHeader);
63
65
  return { baseURL, headers };
64
66
  }
65
67
  function getHttpClientConfig(args) {
@@ -62,7 +62,7 @@ function preprocessSyncCommands(commands) {
62
62
  /**
63
63
  * Makes a request to the Sync API and handles error checking.
64
64
  *
65
- * @param ctx - HTTP identity (auth token, sync API base URI, optional custom fetch).
65
+ * @param ctx - HTTP identity (auth token, sync API base URI, optional custom fetch and headers).
66
66
  * @param syncRequest - The sync request payload.
67
67
  * @param options - Optional request identifier and commands flag.
68
68
  * @returns The sync response data.
@@ -79,6 +79,7 @@ export async function performSyncRequest(ctx, syncRequest, options = {}) {
79
79
  relativePath: ENDPOINT_SYNC,
80
80
  apiToken: ctx.authToken,
81
81
  customFetch: ctx.customFetch,
82
+ customHeaders: ctx.customHeaders,
82
83
  payload: processedRequest,
83
84
  requestId: requestId,
84
85
  hasSyncCommands: hasSyncCommands,
@@ -101,8 +101,7 @@ export const taupe = {
101
101
  };
102
102
  /**
103
103
  * Every color, in the order the apps show them. The keys mirror `TodoistColor` from
104
- * `@doist/sdk-kmp/shared-todoist-all`, which a test checks; the display names and hex values are
105
- * the SDK's own.
104
+ * `@doist/sdk-kmp/shared-todoist-all`; the display names and hex values are the SDK's own.
106
105
  */
107
106
  export const colors = [
108
107
  berryRed,
@@ -11,3 +11,32 @@ export function headersToRecord(headers) {
11
11
  });
12
12
  return result;
13
13
  }
14
+ /**
15
+ * Merges header records left to right. HTTP header names are case-insensitive,
16
+ * so a later record replaces any earlier header with the same name whatever
17
+ * its case. Without this, a caller's `authorization` would sit alongside the
18
+ * SDK's `Authorization` and `fetch` would join the two into one value.
19
+ */
20
+ export function mergeHeaders(...sources) {
21
+ const merged = {};
22
+ for (const source of sources) {
23
+ if (!source)
24
+ continue;
25
+ for (const [name, value] of Object.entries(source)) {
26
+ const lowerName = name.toLowerCase();
27
+ for (const existingName of Object.keys(merged)) {
28
+ if (existingName.toLowerCase() === lowerName)
29
+ delete merged[existingName];
30
+ }
31
+ merged[name] = value;
32
+ }
33
+ }
34
+ return merged;
35
+ }
36
+ /**
37
+ * Turns the `headers` option into the record to send with one request, calling
38
+ * it when it is a function so per-request values are fresh.
39
+ */
40
+ export function resolveCustomHeaders(headers) {
41
+ return typeof headers === 'function' ? headers() : headers;
42
+ }
@@ -22,6 +22,7 @@ import { v4 as uuidv4 } from 'uuid';
22
22
  import { fetchWithRetry } from '../transport/fetch-with-retry.js';
23
23
  import { toTodoistRequestError } from '../transport/http-client.js';
24
24
  import { camelCaseKeys } from './case-conversion.js';
25
+ import { mergeHeaders, resolveCustomHeaders } from './headers.js';
25
26
  /**
26
27
  * Helper function to determine content-type from filename extension.
27
28
  * @param fileName - The filename to analyze
@@ -199,18 +200,12 @@ function buildMultipartBody(args) {
199
200
  * ```
200
201
  */
201
202
  export async function uploadMultipartFile(args) {
202
- const { baseUrl, authToken, endpoint, file, fileName, additionalFields, requestId, customFetch, } = args;
203
+ const { baseUrl, authToken, endpoint, file, fileName, additionalFields, requestId, customHeaders, customFetch, } = args;
203
204
  // Capture original stack for better error reporting
204
205
  const originalStack = new Error();
205
206
  // Build the full URL
206
207
  const url = `${baseUrl}${endpoint}`;
207
208
  let body;
208
- const headers = {
209
- Authorization: `Bearer ${authToken}`,
210
- };
211
- if (requestId) {
212
- headers['X-Request-Id'] = requestId;
213
- }
214
209
  // Resolve the file to something every `fetch` accepts as a body part: a
215
210
  // Blob where the size is known up front, or a Node stream that gets piped
216
211
  // through untouched.
@@ -251,9 +246,11 @@ export async function uploadMultipartFile(args) {
251
246
  getContentTypeFromFileName(resolvedFileName),
252
247
  additionalFields,
253
248
  });
254
- // The boundary is ours, so Content-Type has to be set explicitly — `fetch`
255
- // only fills it in for a `FormData` body.
256
- headers['Content-Type'] = multipart.contentType;
249
+ // Custom headers come first so they can never replace the SDK's own.
250
+ const headers = mergeHeaders(resolveCustomHeaders(customHeaders), Object.assign(Object.assign({ Authorization: `Bearer ${authToken}` }, (requestId ? { 'X-Request-Id': requestId } : {})), {
251
+ // The boundary is ours, so Content-Type has to be set explicitly —
252
+ // `fetch` only fills it in for a `FormData` body.
253
+ 'Content-Type': multipart.contentType }));
257
254
  body = multipart.body;
258
255
  // Make the request using fetch
259
256
  let response;
@@ -1,10 +1,10 @@
1
1
  import type { SyncRequestContext } from '../transport/sync-request.js';
2
- import type { CustomFetch } from '../types/http.js';
2
+ import type { CustomFetch, CustomHeaders } from '../types/http.js';
3
3
  /**
4
4
  * Internal base class for every domain sub-client (tasks, projects, …).
5
5
  *
6
6
  * Not exported from the package — sub-clients are an implementation detail
7
- * of `TodoistApi`. Holds the three pieces of HTTP identity every sub-client
7
+ * of `TodoistApi`. Holds the pieces of HTTP identity every sub-client
8
8
  * needs and exposes a typed accessor to feed the Sync API helper.
9
9
  *
10
10
  * The constructor takes a {@link SyncRequestContext} — the same record
@@ -17,6 +17,7 @@ export declare abstract class BaseClient {
17
17
  protected readonly syncApiBase: string;
18
18
  protected readonly apiRootBase: string;
19
19
  protected readonly customFetch?: CustomFetch;
20
+ protected readonly customHeaders?: CustomHeaders;
20
21
  constructor(context: SyncRequestContext);
21
22
  protected get syncContext(): SyncRequestContext;
22
23
  }
@@ -6,7 +6,7 @@ import type { BillingPortalUrlResponse, CancelPlanArgs, CheckoutSessionResponse,
6
6
  import { Attachment, Comment, AddCommentArgs, UpdateCommentArgs, GetProjectCommentsArgs, GetTaskCommentsArgs, GetCommentsResponse } from './types/comments/index.js';
7
7
  import { GetOrCreateEmailArgs, GetOrCreateEmailResponse, DisableEmailArgs } from './types/emails/index.js';
8
8
  import { GetFoldersArgs, GetFoldersResponse, AddFolderArgs, UpdateFolderArgs } from './types/folders/index.js';
9
- import { CustomFetch, FileResponse } from './types/http.js';
9
+ import { CustomFetch, CustomHeaders, FileResponse } from './types/http.js';
10
10
  import { IdMapping, MovedId, GetIdMappingsArgs, GetMovedIdsArgs } from './types/id-mappings/index.js';
11
11
  import { ProjectActivityStats, ProjectHealth, ProjectHealthContext, ProjectProgress, WorkspaceInsights, GetProjectActivityStatsArgs, GetWorkspaceInsightsArgs } from './types/insights/index.js';
12
12
  import { Label, AddLabelArgs, UpdateLabelArgs, GetLabelsArgs, SearchLabelsArgs, GetLabelsResponse, GetSharedLabelsArgs, GetSharedLabelsResponse, RenameSharedLabelArgs, RemoveSharedLabelArgs } from './types/labels/index.js';
@@ -55,11 +55,19 @@ export type TodoistApiOptions = {
55
55
  * Optional custom fetch function for alternative HTTP clients (e.g., Obsidian's requestUrl, React Native fetch)
56
56
  */
57
57
  customFetch?: CustomFetch;
58
+ /**
59
+ * Optional headers sent with every API request, e.g. `{ 'Doist-Platform': 'cli' }` for client
60
+ * attribution. Pass a function instead to compute them per request, e.g. a fresh request ID;
61
+ * it is called once per request and retries reuse its result. Cannot replace `Authorization`
62
+ * or `X-Request-Id`. Not sent on attachment downloads, which redirect to a third-party CDN.
63
+ */
64
+ headers?: CustomHeaders;
58
65
  };
59
66
  export declare class TodoistApi {
60
67
  private authToken;
61
68
  private syncApiBase;
62
69
  private customFetch?;
70
+ private customHeaders?;
63
71
  private readonly taskClient;
64
72
  private readonly projectClient;
65
73
  private readonly sectionClient;
@@ -1,4 +1,4 @@
1
- import type { CustomFetch, HttpMethod, HttpResponse } from '../types/http.js';
1
+ import type { CustomFetch, CustomHeaders, HttpMethod, HttpResponse } from '../types/http.js';
2
2
  type RequestArgs = {
3
3
  httpMethod: HttpMethod;
4
4
  baseUri: string;
@@ -7,7 +7,7 @@ type RequestArgs = {
7
7
  payload?: Record<string, unknown>;
8
8
  requestId?: string;
9
9
  hasSyncCommands?: boolean;
10
- customHeaders?: Record<string, string>;
10
+ customHeaders?: CustomHeaders;
11
11
  customFetch?: CustomFetch;
12
12
  };
13
13
  export declare function paramsSerializer(params: Record<string, unknown>): string;
@@ -1,4 +1,4 @@
1
- import type { CustomFetch } from '../types/http.js';
1
+ import type { CustomFetch, CustomHeaders } from '../types/http.js';
2
2
  import { type SyncRequest, type SyncResponse } from '../types/sync/index.js';
3
3
  /**
4
4
  * Shared context needed to issue Sync API requests.
@@ -11,11 +11,12 @@ export type SyncRequestContext = {
11
11
  authToken: string;
12
12
  syncApiBase: string;
13
13
  customFetch?: CustomFetch;
14
+ customHeaders?: CustomHeaders;
14
15
  };
15
16
  /**
16
17
  * Makes a request to the Sync API and handles error checking.
17
18
  *
18
- * @param ctx - HTTP identity (auth token, sync API base URI, optional custom fetch).
19
+ * @param ctx - HTTP identity (auth token, sync API base URI, optional custom fetch and headers).
19
20
  * @param syncRequest - The sync request payload.
20
21
  * @param options - Optional request identifier and commands flag.
21
22
  * @returns The sync response data.
@@ -83,6 +83,12 @@ export type CustomFetchResponse = {
83
83
  json(): Promise<unknown>;
84
84
  arrayBuffer?: () => Promise<ArrayBuffer>;
85
85
  };
86
+ /**
87
+ * Extra headers sent with every API request: either a fixed record, or a function
88
+ * the SDK calls once per request (retries reuse its result) for values that change
89
+ * between requests, such as a per-request ID or the command currently running.
90
+ */
91
+ export type CustomHeaders = Record<string, string> | (() => Record<string, string>);
86
92
  /**
87
93
  * Custom fetch function type for alternative HTTP clients
88
94
  */
@@ -101,8 +101,7 @@ export declare const taupe: {
101
101
  };
102
102
  /**
103
103
  * Every color, in the order the apps show them. The keys mirror `TodoistColor` from
104
- * `@doist/sdk-kmp/shared-todoist-all`, which a test checks; the display names and hex values are
105
- * the SDK's own.
104
+ * `@doist/sdk-kmp/shared-todoist-all`; the display names and hex values are the SDK's own.
106
105
  */
107
106
  export declare const colors: readonly [{
108
107
  readonly key: "berry_red";
@@ -1,3 +1,4 @@
1
+ import type { CustomHeaders } from '../types/http.js';
1
2
  /**
2
3
  * Converts a native Fetch `Headers` object into a plain `Record<string, string>`.
3
4
  * Used when wrapping a native `fetch` response into the SDK's `FileResponse`
@@ -5,3 +6,15 @@
5
6
  * `CustomFetchResponse`).
6
7
  */
7
8
  export declare function headersToRecord(headers: Headers): Record<string, string>;
9
+ /**
10
+ * Merges header records left to right. HTTP header names are case-insensitive,
11
+ * so a later record replaces any earlier header with the same name whatever
12
+ * its case. Without this, a caller's `authorization` would sit alongside the
13
+ * SDK's `Authorization` and `fetch` would join the two into one value.
14
+ */
15
+ export declare function mergeHeaders(...sources: (Record<string, string> | undefined)[]): Record<string, string>;
16
+ /**
17
+ * Turns the `headers` option into the record to send with one request, calling
18
+ * it when it is a function so per-request values are fresh.
19
+ */
20
+ export declare function resolveCustomHeaders(headers: CustomHeaders | undefined): Record<string, string> | undefined;
@@ -1,4 +1,4 @@
1
- import type { CustomFetch } from '../types/http.js';
1
+ import type { CustomFetch, CustomHeaders } from '../types/http.js';
2
2
  type UploadMultipartFileArgs = {
3
3
  baseUrl: string;
4
4
  authToken: string;
@@ -7,6 +7,8 @@ type UploadMultipartFileArgs = {
7
7
  fileName?: string;
8
8
  additionalFields: Record<string, string | number | boolean>;
9
9
  requestId?: string;
10
+ /** Extra headers sent with the upload. Cannot replace `Authorization`, `X-Request-Id` or `Content-Type`. */
11
+ customHeaders?: CustomHeaders;
10
12
  customFetch?: CustomFetch;
11
13
  };
12
14
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/todoist-sdk",
3
- "version": "15.5.0",
3
+ "version": "15.7.0",
4
4
  "description": "The official TypeScript SDK for the Todoist REST API.",
5
5
  "author": "Doist developers",
6
6
  "repository": "https://github.com/Doist/todoist-sdk-typescript",
@@ -65,7 +65,7 @@
65
65
  "msw": "2.13.2",
66
66
  "npm-run-all2": "8.0.4",
67
67
  "obsidian": "^1.10.2-1",
68
- "oxfmt": "0.43.0",
68
+ "oxfmt": "0.73.0",
69
69
  "oxlint": "1.57.0",
70
70
  "rimraf": "6.1.3",
71
71
  "semantic-release": "25.0.3",