@doist/todoist-sdk 15.6.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.
package/README.md CHANGED
@@ -50,6 +50,16 @@ const api = new TodoistApi('YOURTOKEN', {
50
50
  })
51
51
  ```
52
52
 
53
+ For values that change between requests, pass a function instead. The SDK calls it once per request, and a retried request reuses its result:
54
+
55
+ ```typescript
56
+ import { randomUUID } from 'node:crypto'
57
+
58
+ const api = new TodoistApi('YOURTOKEN', {
59
+ headers: () => ({ 'Doist-Platform': 'cli', 'Request-Id': randomUUID() }),
60
+ })
61
+ ```
62
+
53
63
  These headers cannot replace `Authorization`, `X-Request-Id` or the multipart `Content-Type`; the SDK's own values always win. They are not sent on attachment downloads (`viewAttachment`), which redirect to a third-party CDN. The OAuth helpers (`getAuthToken`, `refreshAuthToken`, `revokeToken`, …) take their own options and do not send them.
54
64
 
55
65
  ## Custom HTTP Clients
@@ -1,7 +1,7 @@
1
1
  import { ENDPOINT_REST_BACKUPS, ENDPOINT_REST_BACKUPS_DOWNLOAD } from '../consts/endpoints.js';
2
2
  import { request } from '../transport/http-client.js';
3
3
  import { wrapAsFileResponse } from '../utils/file-response.js';
4
- import { mergeHeaders } from '../utils/headers.js';
4
+ import { mergeHeaders, resolveCustomHeaders } from '../utils/headers.js';
5
5
  import { validateBackupArray } from '../utils/validators.js';
6
6
  import { BaseClient } from './base-client.js';
7
7
  /**
@@ -27,7 +27,7 @@ export class BackupClient extends BaseClient {
27
27
  async downloadBackup(args) {
28
28
  const url = `${this.syncApiBase}${ENDPOINT_REST_BACKUPS_DOWNLOAD}?file=${encodeURIComponent(args.file)}`;
29
29
  const fetchOptions = {
30
- headers: mergeHeaders(this.customHeaders, {
30
+ headers: mergeHeaders(resolveCustomHeaders(this.customHeaders), {
31
31
  Authorization: `Bearer ${this.authToken}`,
32
32
  }),
33
33
  };
@@ -3,7 +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 } from '../utils/headers.js';
6
+ import { mergeHeaders, resolveCustomHeaders } from '../utils/headers.js';
7
7
  import { fetchWithRetry } from './fetch-with-retry.js';
8
8
  export function paramsSerializer(params) {
9
9
  const qs = new URLSearchParams();
@@ -61,7 +61,7 @@ function getRequestConfiguration(args) {
61
61
  const authHeader = apiToken ? { Authorization: getAuthHeader(apiToken) } : undefined;
62
62
  const requestIdHeader = requestId ? { 'X-Request-Id': requestId } : undefined;
63
63
  // Custom headers sit below auth and request id so they can never replace them.
64
- const headers = mergeHeaders(defaultHeaders, customHeaders, authHeader, requestIdHeader);
64
+ const headers = mergeHeaders(defaultHeaders, resolveCustomHeaders(customHeaders), authHeader, requestIdHeader);
65
65
  return { baseURL, headers };
66
66
  }
67
67
  function getHttpClientConfig(args) {
@@ -33,3 +33,10 @@ export function mergeHeaders(...sources) {
33
33
  }
34
34
  return merged;
35
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,7 +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 } from './headers.js';
25
+ import { mergeHeaders, resolveCustomHeaders } from './headers.js';
26
26
  /**
27
27
  * Helper function to determine content-type from filename extension.
28
28
  * @param fileName - The filename to analyze
@@ -247,7 +247,7 @@ export async function uploadMultipartFile(args) {
247
247
  additionalFields,
248
248
  });
249
249
  // Custom headers come first so they can never replace the SDK's own.
250
- const headers = mergeHeaders(customHeaders, Object.assign(Object.assign({ Authorization: `Bearer ${authToken}` }, (requestId ? { 'X-Request-Id': requestId } : {})), {
250
+ const headers = mergeHeaders(resolveCustomHeaders(customHeaders), Object.assign(Object.assign({ Authorization: `Bearer ${authToken}` }, (requestId ? { 'X-Request-Id': requestId } : {})), {
251
251
  // The boundary is ours, so Content-Type has to be set explicitly —
252
252
  // `fetch` only fills it in for a `FormData` body.
253
253
  'Content-Type': multipart.contentType }));
@@ -1,5 +1,5 @@
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
  *
@@ -17,7 +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?: Record<string, string>;
20
+ protected readonly customHeaders?: CustomHeaders;
21
21
  constructor(context: SyncRequestContext);
22
22
  protected get syncContext(): SyncRequestContext;
23
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';
@@ -57,10 +57,11 @@ export type TodoistApiOptions = {
57
57
  customFetch?: CustomFetch;
58
58
  /**
59
59
  * Optional headers sent with every API request, e.g. `{ 'Doist-Platform': 'cli' }` for client
60
- * attribution. Cannot replace `Authorization` or `X-Request-Id`. Not sent on attachment
61
- * downloads, which redirect to a third-party CDN.
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.
62
63
  */
63
- headers?: Record<string, string>;
64
+ headers?: CustomHeaders;
64
65
  };
65
66
  export declare class TodoistApi {
66
67
  private authToken;
@@ -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,7 +11,7 @@ export type SyncRequestContext = {
11
11
  authToken: string;
12
12
  syncApiBase: string;
13
13
  customFetch?: CustomFetch;
14
- customHeaders?: Record<string, string>;
14
+ customHeaders?: CustomHeaders;
15
15
  };
16
16
  /**
17
17
  * Makes a request to the Sync API and handles error checking.
@@ -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
  */
@@ -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`
@@ -12,3 +13,8 @@ export declare function headersToRecord(headers: Headers): Record<string, string
12
13
  * SDK's `Authorization` and `fetch` would join the two into one value.
13
14
  */
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;
@@ -8,7 +8,7 @@ type UploadMultipartFileArgs = {
8
8
  additionalFields: Record<string, string | number | boolean>;
9
9
  requestId?: string;
10
10
  /** Extra headers sent with the upload. Cannot replace `Authorization`, `X-Request-Id` or `Content-Type`. */
11
- customHeaders?: Record<string, string>;
11
+ customHeaders?: CustomHeaders;
12
12
  customFetch?: CustomFetch;
13
13
  };
14
14
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/todoist-sdk",
3
- "version": "15.6.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",