@doist/comms-sdk 2.0.1 → 3.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.
- package/dist/esm/utils/multipart-upload.js +73 -15
- package/dist/esm/utils/url-helpers.js +104 -16
- package/dist/types/utils/multipart-upload.d.ts +3 -4
- package/dist/types/utils/url-helpers.d.ts +41 -0
- package/package.json +5 -10
- package/dist/cjs/authentication.js +0 -256
- package/dist/cjs/clients/add-comment-helper.js +0 -43
- package/dist/cjs/clients/attachments-client.js +0 -71
- package/dist/cjs/clients/base-client.js +0 -33
- package/dist/cjs/clients/channels-client.js +0 -211
- package/dist/cjs/clients/comments-client.js +0 -175
- package/dist/cjs/clients/conversation-messages-client.js +0 -169
- package/dist/cjs/clients/conversations-client.js +0 -283
- package/dist/cjs/clients/groups-client.js +0 -172
- package/dist/cjs/clients/hooks-client.js +0 -52
- package/dist/cjs/clients/inbox-client.js +0 -184
- package/dist/cjs/clients/notify-audience.js +0 -45
- package/dist/cjs/clients/reactions-client.js +0 -97
- package/dist/cjs/clients/search-client.js +0 -138
- package/dist/cjs/clients/threads-client.js +0 -364
- package/dist/cjs/clients/users-client.js +0 -326
- package/dist/cjs/clients/workspace-users-client.js +0 -252
- package/dist/cjs/clients/workspaces-client.js +0 -174
- package/dist/cjs/comms-api.js +0 -70
- package/dist/cjs/consts/endpoints.js +0 -34
- package/dist/cjs/index.js +0 -53
- package/dist/cjs/migration.js +0 -96
- package/dist/cjs/package.json +0 -1
- package/dist/cjs/testUtils/msw-handlers.js +0 -51
- package/dist/cjs/testUtils/msw-setup.js +0 -35
- package/dist/cjs/testUtils/obsidian-fetch-adapter.js +0 -53
- package/dist/cjs/testUtils/test-defaults.js +0 -106
- package/dist/cjs/transport/fetch-with-retry.js +0 -146
- package/dist/cjs/transport/http-client.js +0 -56
- package/dist/cjs/transport/http-dispatcher.js +0 -190
- package/dist/cjs/types/api-version.js +0 -8
- package/dist/cjs/types/entities.js +0 -496
- package/dist/cjs/types/enums.js +0 -37
- package/dist/cjs/types/errors.js +0 -12
- package/dist/cjs/types/http.js +0 -4
- package/dist/cjs/types/index.js +0 -22
- package/dist/cjs/types/requests.js +0 -183
- package/dist/cjs/utils/case-conversion.js +0 -54
- package/dist/cjs/utils/index.js +0 -19
- package/dist/cjs/utils/multipart-upload.js +0 -144
- package/dist/cjs/utils/timestamp-conversion.js +0 -58
- package/dist/cjs/utils/url-helpers.js +0 -133
- package/dist/cjs/utils/uuidv7.js +0 -182
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HttpResponse = exports.http = void 0;
|
|
4
|
-
exports.createSuccessResponse = createSuccessResponse;
|
|
5
|
-
exports.createErrorResponse = createErrorResponse;
|
|
6
|
-
exports.apiUrl = apiUrl;
|
|
7
|
-
exports.createGetHandler = createGetHandler;
|
|
8
|
-
exports.createPostHandler = createPostHandler;
|
|
9
|
-
const msw_1 = require("msw");
|
|
10
|
-
Object.defineProperty(exports, "HttpResponse", { enumerable: true, get: function () { return msw_1.HttpResponse; } });
|
|
11
|
-
Object.defineProperty(exports, "http", { enumerable: true, get: function () { return msw_1.http; } });
|
|
12
|
-
const BASE_URL = 'https://comms.todoist.com';
|
|
13
|
-
/**
|
|
14
|
-
* Creates a successful API response with the given data
|
|
15
|
-
*/
|
|
16
|
-
function createSuccessResponse(data) {
|
|
17
|
-
return msw_1.HttpResponse.json(data, { status: 200 });
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Creates an error API response
|
|
21
|
-
*/
|
|
22
|
-
function createErrorResponse(errorCode, errorMessage, status = 400) {
|
|
23
|
-
return msw_1.HttpResponse.json({
|
|
24
|
-
error_code: errorCode,
|
|
25
|
-
error_string: errorMessage,
|
|
26
|
-
}, { status });
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Helper to create API endpoint URL
|
|
30
|
-
*/
|
|
31
|
-
function apiUrl(path) {
|
|
32
|
-
// Remove leading slash if present
|
|
33
|
-
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
|
34
|
-
return `${BASE_URL}/${cleanPath}`;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Creates a handler for a GET endpoint
|
|
38
|
-
*/
|
|
39
|
-
function createGetHandler(endpoint, responseData) {
|
|
40
|
-
return msw_1.http.get(apiUrl(endpoint), () => {
|
|
41
|
-
return createSuccessResponse(responseData);
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Creates a handler for a POST endpoint
|
|
46
|
-
*/
|
|
47
|
-
function createPostHandler(endpoint, responseData) {
|
|
48
|
-
return msw_1.http.post(apiUrl(endpoint), () => {
|
|
49
|
-
return createSuccessResponse(responseData);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.server = void 0;
|
|
4
|
-
const node_1 = require("msw/node");
|
|
5
|
-
const vitest_1 = require("vitest");
|
|
6
|
-
// In production the transport uses undici's own `fetch` (paired with the
|
|
7
|
-
// dispatcher) so the request client and dispatcher stay on one undici version.
|
|
8
|
-
// MSW intercepts the *global* `fetch`, not that separate undici instance, so
|
|
9
|
-
// force the transport onto the global `fetch` for every test in the suite;
|
|
10
|
-
// MSW then intercepts requests as usual. The real undici transport is covered
|
|
11
|
-
// directly by `http-dispatcher.test.ts` and `fetch-with-retry.test.ts`, which
|
|
12
|
-
// opt out of this seam.
|
|
13
|
-
vitest_1.vi.mock('../transport/http-dispatcher', async (importOriginal) => {
|
|
14
|
-
const actual = await importOriginal();
|
|
15
|
-
return {
|
|
16
|
-
...actual,
|
|
17
|
-
getDefaultTransport: vitest_1.vi.fn(async () => undefined),
|
|
18
|
-
};
|
|
19
|
-
});
|
|
20
|
-
// Create MSW server instance
|
|
21
|
-
exports.server = (0, node_1.setupServer)();
|
|
22
|
-
// Start server before all tests
|
|
23
|
-
(0, vitest_1.beforeAll)(() => {
|
|
24
|
-
// Only warn on unhandled requests instead of error, since transport/http-client.test.ts
|
|
25
|
-
// uses direct fetch mocking which bypasses MSW
|
|
26
|
-
exports.server.listen({ onUnhandledRequest: 'warn' });
|
|
27
|
-
});
|
|
28
|
-
// Reset handlers after each test
|
|
29
|
-
(0, vitest_1.afterEach)(() => {
|
|
30
|
-
exports.server.resetHandlers();
|
|
31
|
-
});
|
|
32
|
-
// Clean up after all tests
|
|
33
|
-
(0, vitest_1.afterAll)(() => {
|
|
34
|
-
exports.server.close();
|
|
35
|
-
});
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createObsidianFetchAdapter = createObsidianFetchAdapter;
|
|
4
|
-
/**
|
|
5
|
-
* Creates a CustomFetch adapter for Obsidian's requestUrl API.
|
|
6
|
-
*
|
|
7
|
-
* This adapter bridges the gap between Obsidian's requestUrl interface and the
|
|
8
|
-
* standard fetch-like interface expected by the Comms API SDK.
|
|
9
|
-
*
|
|
10
|
-
* Key differences handled by this adapter:
|
|
11
|
-
* - Obsidian returns response data as properties (response.json, response.text)
|
|
12
|
-
* while the SDK expects methods (response.json(), response.text())
|
|
13
|
-
* - Obsidian's requestUrl bypasses CORS restrictions that would block standard fetch
|
|
14
|
-
* - Obsidian throws on HTTP errors by default; we set throw: false to handle manually
|
|
15
|
-
* - Obsidian doesn't provide statusText; we default to empty string
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```typescript
|
|
19
|
-
* import { requestUrl } from 'obsidian'
|
|
20
|
-
* import { createObsidianFetchAdapter } from './obsidian-fetch-adapter'
|
|
21
|
-
*
|
|
22
|
-
* const api = new CommsApi('your-token', {
|
|
23
|
-
* customFetch: createObsidianFetchAdapter(requestUrl)
|
|
24
|
-
* })
|
|
25
|
-
* ```
|
|
26
|
-
*
|
|
27
|
-
* @param requestUrl - The Obsidian requestUrl function
|
|
28
|
-
* @returns A CustomFetch function compatible with the Comms API SDK
|
|
29
|
-
*/
|
|
30
|
-
function createObsidianFetchAdapter(requestUrl) {
|
|
31
|
-
return async (url, options) => {
|
|
32
|
-
// Build the request parameters in Obsidian's format
|
|
33
|
-
const requestParams = {
|
|
34
|
-
url,
|
|
35
|
-
method: options?.method || 'GET',
|
|
36
|
-
headers: options?.headers,
|
|
37
|
-
body: options?.body,
|
|
38
|
-
throw: false, // Don't throw on HTTP errors; let the SDK handle status codes
|
|
39
|
-
};
|
|
40
|
-
// Make the request using Obsidian's requestUrl
|
|
41
|
-
const response = await requestUrl(requestParams);
|
|
42
|
-
// Transform Obsidian's response format to match CustomFetchResponse interface
|
|
43
|
-
return {
|
|
44
|
-
ok: response.status >= 200 && response.status < 300,
|
|
45
|
-
status: response.status,
|
|
46
|
-
statusText: '', // Obsidian doesn't provide statusText
|
|
47
|
-
headers: response.headers,
|
|
48
|
-
// Wrap Obsidian's direct properties as methods returning promises
|
|
49
|
-
text: () => Promise.resolve(response.text),
|
|
50
|
-
json: () => Promise.resolve(response.json),
|
|
51
|
-
};
|
|
52
|
-
};
|
|
53
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.mockWorkspaceUser = exports.mockComment = exports.mockConversation = exports.mockGroup = exports.mockThread = exports.mockChannel = exports.mockWorkspace = exports.mockUser = exports.TEST_GROUP_ID = exports.TEST_MESSAGE_ID = exports.TEST_CONVERSATION_ID = exports.TEST_COMMENT_ID = exports.TEST_THREAD_ID = exports.TEST_CHANNEL_ID = exports.TEST_API_BASE_URL = exports.TEST_API_TOKEN = void 0;
|
|
4
|
-
const endpoints_1 = require("../consts/endpoints");
|
|
5
|
-
exports.TEST_API_TOKEN = 'test-api-token';
|
|
6
|
-
exports.TEST_API_BASE_URL = (0, endpoints_1.getCommsBaseUri)().replace(/\/$/, '');
|
|
7
|
-
// Stable test IDs. TEST_CHANNEL_ID and TEST_THREAD_ID are base58
|
|
8
|
-
// UUIDv7-shaped because create-thread and add-comment paths validate them
|
|
9
|
-
// SDK-side.
|
|
10
|
-
exports.TEST_CHANNEL_ID = 'CQ7yksHFbYFrSPVTaHVCs';
|
|
11
|
-
exports.TEST_THREAD_ID = 'CQ7yksHFbYFrSPVTaHVCt';
|
|
12
|
-
exports.TEST_COMMENT_ID = '7YpL3oZ4kZ9vP7Q1tR2sX41';
|
|
13
|
-
exports.TEST_CONVERSATION_ID = '7YpL3oZ4kZ9vP7Q1tR2sX42';
|
|
14
|
-
exports.TEST_MESSAGE_ID = '7YpL3oZ4kZ9vP7Q1tR2sX43';
|
|
15
|
-
exports.TEST_GROUP_ID = '7YpL3oZ4kZ9vP7Q1tR2sX44';
|
|
16
|
-
exports.mockUser = {
|
|
17
|
-
id: 1,
|
|
18
|
-
email: 'test@example.com',
|
|
19
|
-
fullName: 'Test User',
|
|
20
|
-
shortName: 'TU',
|
|
21
|
-
timezone: 'America/New_York',
|
|
22
|
-
removed: false,
|
|
23
|
-
lang: 'en',
|
|
24
|
-
};
|
|
25
|
-
exports.mockWorkspace = {
|
|
26
|
-
id: 1,
|
|
27
|
-
name: 'Test Workspace',
|
|
28
|
-
creator: 1,
|
|
29
|
-
created: new Date('2021-01-01T00:00:00Z'),
|
|
30
|
-
};
|
|
31
|
-
exports.mockChannel = {
|
|
32
|
-
id: exports.TEST_CHANNEL_ID,
|
|
33
|
-
name: 'general',
|
|
34
|
-
creator: 1,
|
|
35
|
-
public: true,
|
|
36
|
-
workspaceId: 1,
|
|
37
|
-
archived: false,
|
|
38
|
-
created: new Date('2021-01-01T00:00:00Z'),
|
|
39
|
-
version: 0,
|
|
40
|
-
url: `https://comms.todoist.com/1/ch/${exports.TEST_CHANNEL_ID}/`,
|
|
41
|
-
};
|
|
42
|
-
exports.mockThread = {
|
|
43
|
-
id: exports.TEST_THREAD_ID,
|
|
44
|
-
title: 'Test Thread',
|
|
45
|
-
content: 'This is a test thread',
|
|
46
|
-
creator: 1,
|
|
47
|
-
channelId: exports.TEST_CHANNEL_ID,
|
|
48
|
-
workspaceId: 1,
|
|
49
|
-
commentCount: 0,
|
|
50
|
-
lastUpdated: new Date('2021-01-01T00:00:00Z'),
|
|
51
|
-
pinned: false,
|
|
52
|
-
posted: new Date('2021-01-01T00:00:00Z'),
|
|
53
|
-
snippet: 'This is a test thread',
|
|
54
|
-
snippetCreator: 1,
|
|
55
|
-
isArchived: false,
|
|
56
|
-
url: `https://comms.todoist.com/1/ch/${exports.TEST_CHANNEL_ID}/t/${exports.TEST_THREAD_ID}/`,
|
|
57
|
-
};
|
|
58
|
-
exports.mockGroup = {
|
|
59
|
-
id: exports.TEST_GROUP_ID,
|
|
60
|
-
name: 'Test Group',
|
|
61
|
-
workspaceId: 1,
|
|
62
|
-
userIds: [1, 2, 3],
|
|
63
|
-
version: 0,
|
|
64
|
-
};
|
|
65
|
-
exports.mockConversation = {
|
|
66
|
-
id: exports.TEST_CONVERSATION_ID,
|
|
67
|
-
workspaceId: 1,
|
|
68
|
-
userIds: [1, 2],
|
|
69
|
-
messageCount: 1,
|
|
70
|
-
lastObjIndex: 0,
|
|
71
|
-
snippet: 'Hello there',
|
|
72
|
-
snippetCreators: [1],
|
|
73
|
-
lastActive: new Date('2021-01-01T00:00:00Z'),
|
|
74
|
-
archived: false,
|
|
75
|
-
created: new Date('2021-01-01T00:00:00Z'),
|
|
76
|
-
creator: 1,
|
|
77
|
-
url: `https://comms.todoist.com/1/msg/${exports.TEST_CONVERSATION_ID}/`,
|
|
78
|
-
};
|
|
79
|
-
exports.mockComment = {
|
|
80
|
-
id: exports.TEST_COMMENT_ID,
|
|
81
|
-
content: 'This is a comment',
|
|
82
|
-
creator: 1,
|
|
83
|
-
threadId: exports.TEST_THREAD_ID,
|
|
84
|
-
workspaceId: 1,
|
|
85
|
-
channelId: exports.TEST_CHANNEL_ID,
|
|
86
|
-
posted: new Date('2021-01-01T00:00:00Z'),
|
|
87
|
-
url: `https://comms.todoist.com/1/ch/${exports.TEST_CHANNEL_ID}/t/${exports.TEST_THREAD_ID}/c/${exports.TEST_COMMENT_ID}`,
|
|
88
|
-
};
|
|
89
|
-
exports.mockWorkspaceUser = {
|
|
90
|
-
id: 1,
|
|
91
|
-
fullName: 'Test User',
|
|
92
|
-
email: 'test@example.com',
|
|
93
|
-
userType: 'USER',
|
|
94
|
-
shortName: 'TU',
|
|
95
|
-
firstName: 'Test',
|
|
96
|
-
imageId: null,
|
|
97
|
-
avatarUrls: null,
|
|
98
|
-
dateFormat: null,
|
|
99
|
-
removed: false,
|
|
100
|
-
restricted: null,
|
|
101
|
-
setupPending: null,
|
|
102
|
-
theme: null,
|
|
103
|
-
timeFormat: null,
|
|
104
|
-
timezone: 'America/New_York',
|
|
105
|
-
version: 1,
|
|
106
|
-
};
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.fetchWithRetry = fetchWithRetry;
|
|
4
|
-
const errors_1 = require("../types/errors");
|
|
5
|
-
const case_conversion_1 = require("../utils/case-conversion");
|
|
6
|
-
const timestamp_conversion_1 = require("../utils/timestamp-conversion");
|
|
7
|
-
const http_dispatcher_1 = require("./http-dispatcher");
|
|
8
|
-
async function fetchWithRetry(url, options, maxRetries = 3, customFetch) {
|
|
9
|
-
let lastError;
|
|
10
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
11
|
-
let clearTimeoutFn;
|
|
12
|
-
try {
|
|
13
|
-
let requestSignal = options.signal || undefined;
|
|
14
|
-
if (options.timeout && options.timeout > 0) {
|
|
15
|
-
const timeoutResult = createTimeoutSignal(options.timeout, requestSignal);
|
|
16
|
-
requestSignal = timeoutResult.signal;
|
|
17
|
-
clearTimeoutFn = timeoutResult.clear;
|
|
18
|
-
}
|
|
19
|
-
const response = customFetch
|
|
20
|
-
? await customFetch(url, options)
|
|
21
|
-
: await fetchWithDefaultTransport(url, options, requestSignal);
|
|
22
|
-
const responseText = await response.text();
|
|
23
|
-
let responseData;
|
|
24
|
-
try {
|
|
25
|
-
responseData = responseText ? JSON.parse(responseText) : undefined;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
responseData = responseText;
|
|
29
|
-
}
|
|
30
|
-
if (!response.ok) {
|
|
31
|
-
throw new errors_1.CommsRequestError(`Request failed with status ${response.status}`, response.status, responseData);
|
|
32
|
-
}
|
|
33
|
-
const camelCased = (0, case_conversion_1.camelCaseKeys)(responseData);
|
|
34
|
-
const transformed = (0, timestamp_conversion_1.transformTimestamps)(camelCased);
|
|
35
|
-
if (clearTimeoutFn) {
|
|
36
|
-
clearTimeoutFn();
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
data: transformed,
|
|
40
|
-
status: response.status,
|
|
41
|
-
headers: response.headers,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
if (clearTimeoutFn) {
|
|
46
|
-
clearTimeoutFn();
|
|
47
|
-
}
|
|
48
|
-
lastError = error instanceof Error ? error : new Error('Unknown error');
|
|
49
|
-
if (attempt < maxRetries && isNetworkError(lastError)) {
|
|
50
|
-
const delay = getRetryDelay(attempt + 1);
|
|
51
|
-
if (delay > 0) {
|
|
52
|
-
await sleep(delay);
|
|
53
|
-
}
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (lastError instanceof errors_1.CommsRequestError) {
|
|
60
|
-
throw lastError;
|
|
61
|
-
}
|
|
62
|
-
throw new errors_1.CommsRequestError(lastError?.message ?? 'Request failed');
|
|
63
|
-
}
|
|
64
|
-
async function fetchWithDefaultTransport(url, options, signal) {
|
|
65
|
-
// Read the dispatcher and its paired `fetch` as one value so they can never
|
|
66
|
-
// be mismatched. On Node, `fetch` is undici's own — paired with the
|
|
67
|
-
// dispatcher — so the request client and dispatcher stay on one undici
|
|
68
|
-
// version; otherwise the `decompress` interceptor terminates gzip responses
|
|
69
|
-
// when the global `fetch`'s bundled undici differs. Browser/edge (and Bun)
|
|
70
|
-
// have no paired `fetch` and fall back to the global one.
|
|
71
|
-
const transport = await (0, http_dispatcher_1.getDefaultTransport)();
|
|
72
|
-
// undici's `fetch` and the global `fetch` are the same function at runtime
|
|
73
|
-
// but carry different (undici vs DOM) `RequestInit`/`Response` types. Call
|
|
74
|
-
// through the global signature, which matches the global-typed `options`.
|
|
75
|
-
const fetchImpl = (transport?.fetch ?? fetch);
|
|
76
|
-
const response = transport?.dispatcher
|
|
77
|
-
? await fetchImpl(url, {
|
|
78
|
-
...options,
|
|
79
|
-
signal,
|
|
80
|
-
// @ts-expect-error - dispatcher is valid for Node.js fetch but not in TS types
|
|
81
|
-
dispatcher: transport.dispatcher,
|
|
82
|
-
})
|
|
83
|
-
: await fetchImpl(url, {
|
|
84
|
-
...options,
|
|
85
|
-
signal,
|
|
86
|
-
});
|
|
87
|
-
return convertResponseToCustomFetch(response);
|
|
88
|
-
}
|
|
89
|
-
async function sleep(ms) {
|
|
90
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
91
|
-
}
|
|
92
|
-
const timeoutErrorName = 'TimeoutError';
|
|
93
|
-
function createTimeoutError(timeoutMs) {
|
|
94
|
-
const error = new Error(`Request timeout after ${timeoutMs}ms`);
|
|
95
|
-
error.name = timeoutErrorName;
|
|
96
|
-
return error;
|
|
97
|
-
}
|
|
98
|
-
function isNetworkError(error) {
|
|
99
|
-
return (error.name === 'TypeError' ||
|
|
100
|
-
error.name === timeoutErrorName ||
|
|
101
|
-
error.message.toLowerCase().includes('network'));
|
|
102
|
-
}
|
|
103
|
-
function getRetryDelay(retryCount) {
|
|
104
|
-
return retryCount === 1 ? 0 : 500;
|
|
105
|
-
}
|
|
106
|
-
function createTimeoutSignal(timeoutMs, existingSignal) {
|
|
107
|
-
const controller = new AbortController();
|
|
108
|
-
const timeoutId = setTimeout(() => {
|
|
109
|
-
controller.abort(createTimeoutError(timeoutMs));
|
|
110
|
-
}, timeoutMs);
|
|
111
|
-
let abortHandler;
|
|
112
|
-
function clear() {
|
|
113
|
-
clearTimeout(timeoutId);
|
|
114
|
-
if (existingSignal && abortHandler) {
|
|
115
|
-
existingSignal.removeEventListener('abort', abortHandler);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (existingSignal) {
|
|
119
|
-
if (existingSignal.aborted) {
|
|
120
|
-
clearTimeout(timeoutId);
|
|
121
|
-
controller.abort(existingSignal.reason);
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
abortHandler = () => {
|
|
125
|
-
clearTimeout(timeoutId);
|
|
126
|
-
controller.abort(existingSignal.reason);
|
|
127
|
-
};
|
|
128
|
-
existingSignal.addEventListener('abort', abortHandler, { once: true });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return { signal: controller.signal, clear };
|
|
132
|
-
}
|
|
133
|
-
function convertResponseToCustomFetch(response) {
|
|
134
|
-
const headers = {};
|
|
135
|
-
response.headers.forEach((value, key) => {
|
|
136
|
-
headers[key] = value;
|
|
137
|
-
});
|
|
138
|
-
return {
|
|
139
|
-
ok: response.ok,
|
|
140
|
-
status: response.status,
|
|
141
|
-
statusText: response.statusText,
|
|
142
|
-
headers,
|
|
143
|
-
text: () => response.clone().text(),
|
|
144
|
-
json: () => response.json(),
|
|
145
|
-
};
|
|
146
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.paramsSerializer = paramsSerializer;
|
|
4
|
-
exports.request = request;
|
|
5
|
-
exports.isSuccess = isSuccess;
|
|
6
|
-
const case_conversion_1 = require("../utils/case-conversion");
|
|
7
|
-
const fetch_with_retry_1 = require("./fetch-with-retry");
|
|
8
|
-
function paramsSerializer(params) {
|
|
9
|
-
const qs = new URLSearchParams();
|
|
10
|
-
Object.keys(params).forEach((key) => {
|
|
11
|
-
const value = params[key];
|
|
12
|
-
if (value != null) {
|
|
13
|
-
if (Array.isArray(value)) {
|
|
14
|
-
qs.append(key, value.join(','));
|
|
15
|
-
}
|
|
16
|
-
else {
|
|
17
|
-
qs.append(key, String(value));
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
return qs.toString();
|
|
22
|
-
}
|
|
23
|
-
const defaultHeaders = {
|
|
24
|
-
'Content-Type': 'application/json',
|
|
25
|
-
};
|
|
26
|
-
function getAuthHeader(apiToken) {
|
|
27
|
-
return `Bearer ${apiToken}`;
|
|
28
|
-
}
|
|
29
|
-
function getRequestConfiguration(baseURL, apiToken, requestId) {
|
|
30
|
-
const authHeader = apiToken ? { Authorization: getAuthHeader(apiToken) } : undefined;
|
|
31
|
-
const requestIdHeader = requestId ? { 'X-Request-Id': requestId } : undefined;
|
|
32
|
-
const headers = { ...defaultHeaders, ...authHeader, ...requestIdHeader };
|
|
33
|
-
return { baseURL, headers, timeout: 30000 };
|
|
34
|
-
}
|
|
35
|
-
async function request(args) {
|
|
36
|
-
const { httpMethod, baseUri, relativePath, apiToken, payload, requestId, customFetch } = args;
|
|
37
|
-
const config = getRequestConfiguration(baseUri, apiToken, requestId);
|
|
38
|
-
const url = new URL(relativePath, config.baseURL).toString();
|
|
39
|
-
const options = {
|
|
40
|
-
method: httpMethod,
|
|
41
|
-
headers: config.headers,
|
|
42
|
-
timeout: config.timeout,
|
|
43
|
-
};
|
|
44
|
-
if (httpMethod === 'GET' && payload) {
|
|
45
|
-
const searchParams = paramsSerializer((0, case_conversion_1.snakeCaseKeys)(payload));
|
|
46
|
-
const urlWithParams = searchParams ? `${url}?${searchParams}` : url;
|
|
47
|
-
return (0, fetch_with_retry_1.fetchWithRetry)(urlWithParams, options, 3, customFetch);
|
|
48
|
-
}
|
|
49
|
-
if (payload && httpMethod !== 'GET') {
|
|
50
|
-
options.body = JSON.stringify((0, case_conversion_1.snakeCaseKeys)(payload));
|
|
51
|
-
}
|
|
52
|
-
return (0, fetch_with_retry_1.fetchWithRetry)(url, options, 3, customFetch);
|
|
53
|
-
}
|
|
54
|
-
function isSuccess(response) {
|
|
55
|
-
return response.status >= 200 && response.status < 300;
|
|
56
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.getDefaultTransport = getDefaultTransport;
|
|
37
|
-
exports.getDefaultDispatcher = getDefaultDispatcher;
|
|
38
|
-
exports.closeDefaultDispatcher = closeDefaultDispatcher;
|
|
39
|
-
exports.resetDefaultDispatcherForTests = resetDefaultDispatcherForTests;
|
|
40
|
-
exports.getDefaultFetch = getDefaultFetch;
|
|
41
|
-
exports.suppressExperimentalWarningsSync = suppressExperimentalWarningsSync;
|
|
42
|
-
let defaultTransport;
|
|
43
|
-
let defaultTransportPromise;
|
|
44
|
-
/**
|
|
45
|
-
* The default dispatcher and its paired `fetch`, as a single value so the two
|
|
46
|
-
* are always read consistently. Resolves to `undefined` outside Node (browser/
|
|
47
|
-
* edge), where callers use the global `fetch` with no dispatcher.
|
|
48
|
-
*/
|
|
49
|
-
async function getDefaultTransport() {
|
|
50
|
-
if (defaultTransport) {
|
|
51
|
-
return defaultTransport;
|
|
52
|
-
}
|
|
53
|
-
if (!defaultTransportPromise) {
|
|
54
|
-
defaultTransportPromise = createDefaultTransport()
|
|
55
|
-
.then((transport) => {
|
|
56
|
-
defaultTransport = transport;
|
|
57
|
-
return transport;
|
|
58
|
-
})
|
|
59
|
-
.catch((error) => {
|
|
60
|
-
defaultTransport = undefined;
|
|
61
|
-
defaultTransportPromise = undefined;
|
|
62
|
-
throw error;
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
return defaultTransportPromise;
|
|
66
|
-
}
|
|
67
|
-
async function getDefaultDispatcher() {
|
|
68
|
-
return (await getDefaultTransport())?.dispatcher;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Drains the default dispatcher's connection pool. CLIs and scripts should
|
|
72
|
-
* `await` this before exit so Node's event loop empties immediately instead
|
|
73
|
-
* of waiting ~4s on keep-alive. No-op in the browser branch.
|
|
74
|
-
*/
|
|
75
|
-
async function closeDefaultDispatcher() {
|
|
76
|
-
// Clear the singleton *before* awaiting init, so any concurrent
|
|
77
|
-
// `getDefaultTransport()` after this point creates a fresh dispatcher
|
|
78
|
-
// instead of receiving a reference to the one we're about to close.
|
|
79
|
-
const initPromise = defaultTransportPromise;
|
|
80
|
-
defaultTransport = undefined;
|
|
81
|
-
defaultTransportPromise = undefined;
|
|
82
|
-
if (!initPromise)
|
|
83
|
-
return;
|
|
84
|
-
try {
|
|
85
|
-
const transport = await initPromise;
|
|
86
|
-
if (transport) {
|
|
87
|
-
await transport.dispatcher.close();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
// init failed; nothing to close
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
function resetDefaultDispatcherForTests() {
|
|
95
|
-
defaultTransport = undefined;
|
|
96
|
-
defaultTransportPromise = undefined;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* The `fetch` implementation that must be used with the default dispatcher.
|
|
100
|
-
* Returns undici's own `fetch` on the full-undici Node path, or `undefined`
|
|
101
|
-
* (meaning: use the global `fetch`) in the browser/edge/Bun paths.
|
|
102
|
-
*
|
|
103
|
-
* Node's global `fetch` is backed by whatever undici version ships inside that
|
|
104
|
-
* Node release (6.x on Node 22 … 8.x on Node 26). Our dispatcher — and its
|
|
105
|
-
* `decompress` interceptor — comes from the npm `undici` package, which is a
|
|
106
|
-
* different version. Handing an npm-undici dispatcher to a mismatched built-in
|
|
107
|
-
* client makes gzip responses fail mid-stream with `terminated`. Sourcing
|
|
108
|
-
* `fetch` from the same npm `undici` keeps the whole request path on one
|
|
109
|
-
* version and removes the split.
|
|
110
|
-
*
|
|
111
|
-
* Only meaningful after {@link getDefaultTransport} has resolved, which is the
|
|
112
|
-
* one place that populates it.
|
|
113
|
-
*/
|
|
114
|
-
function getDefaultFetch() {
|
|
115
|
-
return defaultTransport?.fetch;
|
|
116
|
-
}
|
|
117
|
-
function isNodeEnvironment() {
|
|
118
|
-
return typeof process !== 'undefined' && typeof process.versions?.node === 'string';
|
|
119
|
-
}
|
|
120
|
-
async function createDefaultTransport() {
|
|
121
|
-
if (!isNodeEnvironment()) {
|
|
122
|
-
return undefined;
|
|
123
|
-
}
|
|
124
|
-
// Dynamic import so non-Node consumers (browser, edge runtimes) don't pull
|
|
125
|
-
// undici into their bundle. `isNodeEnvironment()` above already gated this
|
|
126
|
-
// branch, so undici is safe to load when we get here.
|
|
127
|
-
const { EnvHttpProxyAgent, interceptors, fetch: undiciFetch } = await Promise.resolve().then(() => __importStar(require('undici')));
|
|
128
|
-
// `EnvHttpProxyAgent` (with `allowH2`) and `interceptors.decompress()` both
|
|
129
|
-
// emit an ExperimentalWarning on first use, so build the whole dispatcher
|
|
130
|
-
// inside the suppression block.
|
|
131
|
-
return suppressExperimentalWarningsSync(() => {
|
|
132
|
-
// `allowH2: true` opts into HTTP/2 via ALPN; undici falls back to h1.1
|
|
133
|
-
// when the server doesn't negotiate h2. Without this flag undici
|
|
134
|
-
// defaults to h1.1 even when the server supports h2.
|
|
135
|
-
const agent = new EnvHttpProxyAgent({ allowH2: true });
|
|
136
|
-
// Some runtimes report `process.versions.node` (so `isNodeEnvironment()`
|
|
137
|
-
// passes) but ship only a partial undici: `interceptors.decompress` is
|
|
138
|
-
// absent and dispatchers have no `.compose`. Bun is the common case.
|
|
139
|
-
// There the proxy agent alone is enough — Bun's `fetch` decompresses
|
|
140
|
-
// gzip/deflate/br/zstd natively — so skip the interceptor instead of
|
|
141
|
-
// crashing on the missing API. Optional chaining also guards a runtime
|
|
142
|
-
// that omits the `interceptors` export entirely.
|
|
143
|
-
if (typeof interceptors?.decompress !== 'function') {
|
|
144
|
-
// Bun: pair the agent with the global `fetch` (undefined), which
|
|
145
|
-
// decompresses natively.
|
|
146
|
-
return { dispatcher: agent, fetch: undefined };
|
|
147
|
-
}
|
|
148
|
-
// `interceptors.decompress()` decodes gzip/deflate/br/zstd bodies. On
|
|
149
|
-
// Node 24+, attaching any custom dispatcher to global `fetch` strips
|
|
150
|
-
// `content-encoding` without actually decompressing the body.
|
|
151
|
-
// See https://github.com/Doist/todoist-cli/issues/318.
|
|
152
|
-
const decompress = interceptors.decompress();
|
|
153
|
-
// Pair undici's own `fetch` with this dispatcher so the request client
|
|
154
|
-
// and the dispatcher stay on one undici version (see `getDefaultFetch`).
|
|
155
|
-
// The global `fetch` is backed by a different, Node-bundled undici;
|
|
156
|
-
// mixing the two makes the decompress interceptor terminate gzip
|
|
157
|
-
// responses on some Node versions.
|
|
158
|
-
return { dispatcher: agent.compose(decompress), fetch: undiciFetch };
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
// `suppressExperimentalWarningsSync` is exported for direct unit testing —
|
|
162
|
-
// the integration path through `getDefaultDispatcher()` can't reliably
|
|
163
|
-
// exercise it because both the dispatcher singleton and undici's internal
|
|
164
|
-
// `warningEmitted` flag are once-per-process.
|
|
165
|
-
//
|
|
166
|
-
// `fn` must be synchronous so the override covers a single critical section
|
|
167
|
-
// (microseconds) — no unrelated `ExperimentalWarning` from elsewhere can
|
|
168
|
-
// interleave and be lost. We suppress every `ExperimentalWarning` rather than
|
|
169
|
-
// pattern-matching the message text: the message wording is an undici
|
|
170
|
-
// implementation detail (not a stable API), and the suppression window is
|
|
171
|
-
// narrow enough that a coarse type filter is safe.
|
|
172
|
-
function suppressExperimentalWarningsSync(fn) {
|
|
173
|
-
const originalEmit = process.emitWarning;
|
|
174
|
-
process.emitWarning = ((warning, typeOrOptions, ...rest) => {
|
|
175
|
-
const type = typeof typeOrOptions === 'string'
|
|
176
|
-
? typeOrOptions
|
|
177
|
-
: typeof typeOrOptions === 'object' && typeOrOptions !== null
|
|
178
|
-
? typeOrOptions.type
|
|
179
|
-
: undefined;
|
|
180
|
-
if (type === 'ExperimentalWarning')
|
|
181
|
-
return;
|
|
182
|
-
originalEmit.call(process, warning, typeOrOptions, ...rest);
|
|
183
|
-
});
|
|
184
|
-
try {
|
|
185
|
-
return fn();
|
|
186
|
-
}
|
|
187
|
-
finally {
|
|
188
|
-
process.emitWarning = originalEmit;
|
|
189
|
-
}
|
|
190
|
-
}
|