@doist/comms-sdk 3.6.0 → 3.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.
@@ -1,4 +1,5 @@
1
1
  import { snakeCaseKeys } from '../utils/case-conversion.js';
2
+ import { mergeHeaders, resolveCustomHeaders } from '../utils/headers.js';
2
3
  import { fetchWithRetry } from './fetch-with-retry.js';
3
4
  export function paramsSerializer(params) {
4
5
  const qs = new URLSearchParams();
@@ -25,7 +26,7 @@ function getRequestConfiguration(baseURL, apiToken, requestId, customHeaders) {
25
26
  const authHeader = apiToken ? { Authorization: getAuthHeader(apiToken) } : undefined;
26
27
  const requestIdHeader = requestId ? { 'X-Request-Id': requestId } : undefined;
27
28
  // Custom headers sit below auth and request id so they can never clobber them.
28
- const headers = { ...defaultHeaders, ...customHeaders, ...authHeader, ...requestIdHeader };
29
+ const headers = mergeHeaders(defaultHeaders, resolveCustomHeaders(customHeaders), authHeader, requestIdHeader);
29
30
  return { baseURL, headers, timeout: 30000 };
30
31
  }
31
32
  export async function request(args) {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Merges header records left to right. HTTP header names are case-insensitive,
3
+ * so a later record replaces any earlier header with the same name whatever
4
+ * its case. Without this, a caller's `authorization` would sit alongside the
5
+ * SDK's `Authorization` and `fetch` would join the two into one value.
6
+ */
7
+ export function mergeHeaders(...sources) {
8
+ const merged = {};
9
+ for (const source of sources) {
10
+ if (!source)
11
+ continue;
12
+ for (const [name, value] of Object.entries(source)) {
13
+ const lowerName = name.toLowerCase();
14
+ for (const existingName of Object.keys(merged)) {
15
+ if (existingName.toLowerCase() === lowerName)
16
+ delete merged[existingName];
17
+ }
18
+ merged[name] = value;
19
+ }
20
+ }
21
+ return merged;
22
+ }
23
+ /**
24
+ * Turns the `headers` option into the record to send with one request, calling
25
+ * it when it is a function so per-request values are fresh.
26
+ */
27
+ export function resolveCustomHeaders(headers) {
28
+ return typeof headers === 'function' ? headers() : headers;
29
+ }
@@ -1,5 +1,6 @@
1
1
  import { v4 as uuid } from 'uuid';
2
2
  import { fetchWithRetry } from '../transport/fetch-with-retry.js';
3
+ import { mergeHeaders, resolveCustomHeaders } from './headers.js';
3
4
  /**
4
5
  * Determine a content-type from a filename extension. Falls back to
5
6
  * `application/octet-stream` for unknown extensions.
@@ -178,16 +179,14 @@ export async function uploadMultipartFile(args) {
178
179
  contentType: resolvedType,
179
180
  fields,
180
181
  });
181
- const headers = {
182
- ...customHeaders,
182
+ // Custom headers come first so they can never replace the SDK's own.
183
+ const headers = mergeHeaders(resolveCustomHeaders(customHeaders), {
183
184
  Authorization: `Bearer ${authToken}`,
184
185
  // The boundary is ours, so this has to be declared explicitly — `fetch`
185
186
  // only fills it in for a `FormData` body.
186
187
  'Content-Type': multipart.contentType,
187
- };
188
- if (requestId) {
189
- headers['X-Request-Id'] = requestId;
190
- }
188
+ ...(requestId ? { 'X-Request-Id': requestId } : {}),
189
+ });
191
190
  const url = new URL(endpoint, baseUrl).toString();
192
191
  const response = await fetchWithRetry(url, {
193
192
  method: 'POST',
@@ -1,11 +1,11 @@
1
1
  import { type Comment, type createCommentSchema } from '../types/entities.js';
2
- import type { CustomFetch } from '../types/http.js';
2
+ import type { CustomFetch, CustomHeaders } from '../types/http.js';
3
3
  import type { CreateCommentArgs, ThreadAction } from '../types/requests.js';
4
4
  type ClientContext = {
5
5
  baseUri: string;
6
6
  apiToken: string;
7
7
  customFetch?: CustomFetch;
8
- headers?: Record<string, string>;
8
+ headers?: CustomHeaders;
9
9
  /** Per-client Comment schema, base-bound for the returned comment's web `url`. */
10
10
  schema: ReturnType<typeof createCommentSchema>;
11
11
  };
@@ -1,5 +1,5 @@
1
1
  import type { ApiVersion } from '../types/api-version.js';
2
- import type { CustomFetch } from '../types/http.js';
2
+ import type { CustomFetch, CustomHeaders } from '../types/http.js';
3
3
  export type ClientConfig = {
4
4
  /** API token for authentication */
5
5
  apiToken: string;
@@ -9,8 +9,8 @@ export type ClientConfig = {
9
9
  version?: ApiVersion;
10
10
  /** Optional custom fetch implementation for cross-platform compatibility */
11
11
  customFetch?: CustomFetch;
12
- /** Optional headers sent with every request */
13
- headers?: Record<string, string>;
12
+ /** Optional headers sent with every request, or a function called once per request */
13
+ headers?: CustomHeaders;
14
14
  };
15
15
  /**
16
16
  * Base class for every Comms API client. Centralizes URL handling and
@@ -21,7 +21,7 @@ export declare class BaseClient {
21
21
  protected readonly baseUrl?: string;
22
22
  protected readonly defaultVersion: ApiVersion;
23
23
  protected readonly customFetch?: CustomFetch;
24
- protected readonly headers?: Record<string, string>;
24
+ protected readonly headers?: CustomHeaders;
25
25
  constructor(config: ClientConfig);
26
26
  /**
27
27
  * Returns the base URI for an API request, with a guaranteed trailing
@@ -13,7 +13,7 @@ import { UsersClient } from './clients/users-client.js';
13
13
  import { WorkspaceUsersClient } from './clients/workspace-users-client.js';
14
14
  import { WorkspacesClient } from './clients/workspaces-client.js';
15
15
  import type { ApiVersion } from './types/api-version.js';
16
- import type { CustomFetch } from './types/http.js';
16
+ import type { CustomFetch, CustomHeaders } from './types/http.js';
17
17
  export type CommsApiOptions = {
18
18
  /** Optional custom API base URL. If not provided, defaults to Comms' standard API endpoint. */
19
19
  baseUrl?: string;
@@ -21,8 +21,13 @@ export type CommsApiOptions = {
21
21
  version?: ApiVersion;
22
22
  /** Optional custom fetch implementation for cross-platform compatibility (e.g., Obsidian, React Native, Electron). */
23
23
  customFetch?: CustomFetch;
24
- /** Optional headers sent with every request, e.g. `{ 'Doist-Platform': 'cli' }` for client attribution. Cannot override `Authorization` or `X-Request-Id`. */
25
- headers?: Record<string, string>;
24
+ /**
25
+ * Optional headers sent with every request, e.g. `{ 'Doist-Platform': 'cli' }` for client
26
+ * attribution. Pass a function instead to compute them per request, e.g. a fresh request ID;
27
+ * it is called once per request and retries reuse its result. Cannot override `Authorization`
28
+ * or `X-Request-Id`.
29
+ */
30
+ headers?: CustomHeaders;
26
31
  };
27
32
  /**
28
33
  * The main API client for interacting with the Comms REST API.
@@ -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
  export declare function paramsSerializer(params: Record<string, unknown>): string;
3
3
  export type RequestArgs = {
4
4
  httpMethod: HttpMethod;
@@ -7,7 +7,7 @@ export type RequestArgs = {
7
7
  apiToken?: string;
8
8
  payload?: Record<string, unknown>;
9
9
  requestId?: string;
10
- headers?: Record<string, string>;
10
+ headers?: CustomHeaders;
11
11
  customFetch?: CustomFetch;
12
12
  };
13
13
  export declare function request<T>(args: RequestArgs): Promise<HttpResponse<T>>;
@@ -5,6 +5,12 @@ export type HttpResponse<T = unknown> = {
5
5
  status: number;
6
6
  headers: Record<string, string>;
7
7
  };
8
+ /**
9
+ * Extra headers sent with every API request: either a fixed record, or a function
10
+ * the SDK calls once per request (retries reuse its result) for values that change
11
+ * between requests, such as a per-request ID or the command currently running.
12
+ */
13
+ export type CustomHeaders = Record<string, string> | (() => Record<string, string>);
8
14
  export type RequestConfig = {
9
15
  baseURL?: string;
10
16
  headers?: Record<string, string>;
@@ -0,0 +1,13 @@
1
+ import type { CustomHeaders } from '../types/http.js';
2
+ /**
3
+ * Merges header records left to right. HTTP header names are case-insensitive,
4
+ * so a later record replaces any earlier header with the same name whatever
5
+ * its case. Without this, a caller's `authorization` would sit alongside the
6
+ * SDK's `Authorization` and `fetch` would join the two into one value.
7
+ */
8
+ export declare function mergeHeaders(...sources: (Record<string, string> | undefined)[]): Record<string, string>;
9
+ /**
10
+ * Turns the `headers` option into the record to send with one request, calling
11
+ * it when it is a function so per-request values are fresh.
12
+ */
13
+ 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
  /**
3
3
  * File content accepted by the upload helpers.
4
4
  *
@@ -33,8 +33,8 @@ type UploadMultipartFileArgs = {
33
33
  maxRetries?: number;
34
34
  /** Optional request ID for tracing. */
35
35
  requestId?: string;
36
- /** Optional headers sent with the request. Cannot override `Authorization` or `Content-Type`. */
37
- headers?: Record<string, string>;
36
+ /** Optional headers sent with the request. Cannot override `Authorization`, `X-Request-Id` or `Content-Type`. */
37
+ headers?: CustomHeaders;
38
38
  /** Optional custom fetch implementation. */
39
39
  customFetch?: CustomFetch;
40
40
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/comms-sdk",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "A TypeScript wrapper for the Comms REST API.",
5
5
  "author": "Doist developers",
6
6
  "homepage": "https://doist.github.io/comms-sdk-typescript/",