@doist/todoist-sdk 10.2.0-next.1 → 10.2.0-next.3
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/cjs/clients/app-client.js +1 -2
- package/dist/cjs/clients/backup-client.js +3 -21
- package/dist/cjs/clients/template-client.js +2 -3
- package/dist/cjs/clients/ui-extension-client.js +1 -2
- package/dist/cjs/clients/upload-client.js +3 -22
- package/dist/cjs/test-utils/obsidian-fetch-adapter.js +1 -0
- package/dist/cjs/transport/fetch-with-retry.js +1 -0
- package/dist/cjs/transport/http-dispatcher.js +48 -2
- package/dist/cjs/utils/file-response.js +32 -0
- package/dist/cjs/utils/multipart-upload.js +2 -1
- package/dist/esm/clients/app-client.js +1 -2
- package/dist/esm/clients/backup-client.js +3 -21
- package/dist/esm/clients/template-client.js +2 -3
- package/dist/esm/clients/ui-extension-client.js +1 -2
- package/dist/esm/clients/upload-client.js +3 -22
- package/dist/esm/test-utils/obsidian-fetch-adapter.js +1 -0
- package/dist/esm/transport/fetch-with-retry.js +1 -0
- package/dist/esm/transport/http-dispatcher.js +47 -2
- package/dist/esm/utils/file-response.js +29 -0
- package/dist/esm/utils/multipart-upload.js +2 -1
- package/dist/types/transport/http-dispatcher.d.ts +1 -0
- package/dist/types/types/http.d.ts +7 -1
- package/dist/types/utils/file-response.d.ts +15 -0
- package/dist/types/utils/multipart-upload.d.ts +1 -1
- package/package.json +2 -2
|
@@ -16,7 +16,6 @@ const zod_1 = require("zod");
|
|
|
16
16
|
const endpoints_1 = require("../consts/endpoints");
|
|
17
17
|
const http_client_1 = require("../transport/http-client");
|
|
18
18
|
const apps_1 = require("../types/apps");
|
|
19
|
-
const case_conversion_1 = require("../utils/case-conversion");
|
|
20
19
|
const multipart_upload_1 = require("../utils/multipart-upload");
|
|
21
20
|
const validators_1 = require("../utils/validators");
|
|
22
21
|
const base_client_1 = require("./base-client");
|
|
@@ -145,7 +144,7 @@ class AppClient extends base_client_1.BaseClient {
|
|
|
145
144
|
requestId: requestId,
|
|
146
145
|
customFetch: this.customFetch,
|
|
147
146
|
});
|
|
148
|
-
return (0, validators_1.validateAppWithUserCount)(
|
|
147
|
+
return (0, validators_1.validateAppWithUserCount)(data);
|
|
149
148
|
}
|
|
150
149
|
async getAppTestToken(appId, requestId) {
|
|
151
150
|
validators_1.IdSchema.parse(appId);
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.BackupClient = void 0;
|
|
4
4
|
const endpoints_1 = require("../consts/endpoints");
|
|
5
5
|
const http_client_1 = require("../transport/http-client");
|
|
6
|
-
const
|
|
6
|
+
const file_response_1 = require("../utils/file-response");
|
|
7
7
|
const validators_1 = require("../utils/validators");
|
|
8
8
|
const base_client_1 = require("./base-client");
|
|
9
9
|
/**
|
|
@@ -35,31 +35,13 @@ class BackupClient extends base_client_1.BaseClient {
|
|
|
35
35
|
if (!response.ok) {
|
|
36
36
|
throw new Error(`Failed to download backup: ${response.status} ${response.statusText}`);
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
const buffer = new TextEncoder().encode(text).buffer;
|
|
40
|
-
return {
|
|
41
|
-
ok: response.ok,
|
|
42
|
-
status: response.status,
|
|
43
|
-
statusText: response.statusText,
|
|
44
|
-
headers: response.headers,
|
|
45
|
-
text: () => Promise.resolve(text),
|
|
46
|
-
json: () => response.json(),
|
|
47
|
-
arrayBuffer: () => Promise.resolve(buffer),
|
|
48
|
-
};
|
|
38
|
+
return (0, file_response_1.wrapAsFileResponse)(response, 'downloadBackup');
|
|
49
39
|
}
|
|
50
40
|
const response = await fetch(url, fetchOptions);
|
|
51
41
|
if (!response.ok) {
|
|
52
42
|
throw new Error(`Failed to download backup: ${response.status} ${response.statusText}`);
|
|
53
43
|
}
|
|
54
|
-
return
|
|
55
|
-
ok: response.ok,
|
|
56
|
-
status: response.status,
|
|
57
|
-
statusText: response.statusText,
|
|
58
|
-
headers: (0, headers_1.headersToRecord)(response.headers),
|
|
59
|
-
text: () => response.text(),
|
|
60
|
-
json: () => response.json(),
|
|
61
|
-
arrayBuffer: () => response.arrayBuffer(),
|
|
62
|
-
};
|
|
44
|
+
return (0, file_response_1.wrapAsFileResponse)(response, 'downloadBackup');
|
|
63
45
|
}
|
|
64
46
|
}
|
|
65
47
|
exports.BackupClient = BackupClient;
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.TemplateClient = void 0;
|
|
4
4
|
const endpoints_1 = require("../consts/endpoints");
|
|
5
5
|
const http_client_1 = require("../transport/http-client");
|
|
6
|
-
const case_conversion_1 = require("../utils/case-conversion");
|
|
7
6
|
const multipart_upload_1 = require("../utils/multipart-upload");
|
|
8
7
|
const validators_1 = require("../utils/validators");
|
|
9
8
|
const base_client_1 = require("./base-client");
|
|
@@ -53,7 +52,7 @@ class TemplateClient extends base_client_1.BaseClient {
|
|
|
53
52
|
customFetch: this.customFetch,
|
|
54
53
|
requestId,
|
|
55
54
|
});
|
|
56
|
-
return this.validateTemplateResponse(
|
|
55
|
+
return this.validateTemplateResponse(data);
|
|
57
56
|
}
|
|
58
57
|
async importTemplateIntoProject(args, requestId) {
|
|
59
58
|
const { file, fileName, projectId } = args;
|
|
@@ -67,7 +66,7 @@ class TemplateClient extends base_client_1.BaseClient {
|
|
|
67
66
|
customFetch: this.customFetch,
|
|
68
67
|
requestId,
|
|
69
68
|
});
|
|
70
|
-
return this.validateTemplateResponse(
|
|
69
|
+
return this.validateTemplateResponse(data);
|
|
71
70
|
}
|
|
72
71
|
async importTemplateFromId(args, requestId) {
|
|
73
72
|
const { data } = await (0, http_client_1.request)({
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.UiExtensionClient = void 0;
|
|
4
4
|
const endpoints_1 = require("../consts/endpoints");
|
|
5
5
|
const http_client_1 = require("../transport/http-client");
|
|
6
|
-
const case_conversion_1 = require("../utils/case-conversion");
|
|
7
6
|
const multipart_upload_1 = require("../utils/multipart-upload");
|
|
8
7
|
const validators_1 = require("../utils/validators");
|
|
9
8
|
const base_client_1 = require("./base-client");
|
|
@@ -102,7 +101,7 @@ class UiExtensionClient extends base_client_1.BaseClient {
|
|
|
102
101
|
requestId: requestId,
|
|
103
102
|
customFetch: this.customFetch,
|
|
104
103
|
});
|
|
105
|
-
return (0, validators_1.validateUiExtension)(
|
|
104
|
+
return (0, validators_1.validateUiExtension)(data);
|
|
106
105
|
}
|
|
107
106
|
}
|
|
108
107
|
exports.UiExtensionClient = UiExtensionClient;
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.UploadClient = void 0;
|
|
4
4
|
const endpoints_1 = require("../consts/endpoints");
|
|
5
5
|
const http_client_1 = require("../transport/http-client");
|
|
6
|
-
const
|
|
6
|
+
const file_response_1 = require("../utils/file-response");
|
|
7
7
|
const multipart_upload_1 = require("../utils/multipart-upload");
|
|
8
8
|
const validators_1 = require("../utils/validators");
|
|
9
9
|
const base_client_1 = require("./base-client");
|
|
@@ -71,32 +71,13 @@ class UploadClient extends base_client_1.BaseClient {
|
|
|
71
71
|
if (!response.ok) {
|
|
72
72
|
throw new Error(`Failed to fetch attachment: ${response.status} ${response.statusText}`);
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
const text = await response.text();
|
|
76
|
-
const buffer = new TextEncoder().encode(text).buffer;
|
|
77
|
-
return {
|
|
78
|
-
ok: response.ok,
|
|
79
|
-
status: response.status,
|
|
80
|
-
statusText: response.statusText,
|
|
81
|
-
headers: response.headers,
|
|
82
|
-
text: () => Promise.resolve(text),
|
|
83
|
-
json: () => response.json(),
|
|
84
|
-
arrayBuffer: () => Promise.resolve(buffer),
|
|
85
|
-
};
|
|
74
|
+
return (0, file_response_1.wrapAsFileResponse)(response, 'viewAttachment');
|
|
86
75
|
}
|
|
87
76
|
const response = await fetch(fileUrl, fetchOptions);
|
|
88
77
|
if (!response.ok) {
|
|
89
78
|
throw new Error(`Failed to fetch attachment: ${response.status} ${response.statusText}`);
|
|
90
79
|
}
|
|
91
|
-
return
|
|
92
|
-
ok: response.ok,
|
|
93
|
-
status: response.status,
|
|
94
|
-
statusText: response.statusText,
|
|
95
|
-
headers: (0, headers_1.headersToRecord)(response.headers),
|
|
96
|
-
text: () => response.text(),
|
|
97
|
-
json: () => response.json(),
|
|
98
|
-
arrayBuffer: () => response.arrayBuffer(),
|
|
99
|
-
};
|
|
80
|
+
return (0, file_response_1.wrapAsFileResponse)(response, 'viewAttachment');
|
|
100
81
|
}
|
|
101
82
|
}
|
|
102
83
|
exports.UploadClient = UploadClient;
|
|
@@ -48,6 +48,7 @@ function createObsidianFetchAdapter(requestUrl) {
|
|
|
48
48
|
// Wrap Obsidian's direct properties as methods returning promises
|
|
49
49
|
text: () => Promise.resolve(response.text),
|
|
50
50
|
json: () => Promise.resolve(response.json),
|
|
51
|
+
arrayBuffer: () => Promise.resolve(response.arrayBuffer),
|
|
51
52
|
};
|
|
52
53
|
};
|
|
53
54
|
}
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.getDefaultDispatcher = getDefaultDispatcher;
|
|
37
37
|
exports.resetDefaultDispatcherForTests = resetDefaultDispatcherForTests;
|
|
38
|
+
exports.suppressExperimentalWarningsSync = suppressExperimentalWarningsSync;
|
|
38
39
|
// Use effectively-disabled keep-alive so short-lived CLI processes do not stay
|
|
39
40
|
// open waiting on idle sockets. Undici requires positive values, so we use 1ms.
|
|
40
41
|
const KEEP_ALIVE_OPTIONS = {
|
|
@@ -63,8 +64,53 @@ async function resetDefaultDispatcherForTests() {
|
|
|
63
64
|
await dispatcherPromise.then((dispatcher) => dispatcher.close(), () => undefined);
|
|
64
65
|
}
|
|
65
66
|
async function createDefaultDispatcher() {
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
// Dynamic import so non-Node consumers (browser, edge runtimes) don't pull
|
|
68
|
+
// undici into their bundle. `getDefaultDispatcher` already short-circuits
|
|
69
|
+
// outside Node, so this branch only runs when undici is safe to load.
|
|
70
|
+
const { EnvHttpProxyAgent, interceptors } = await Promise.resolve().then(() => __importStar(require('undici')));
|
|
71
|
+
// Compose the response-decompression interceptor so gzip/deflate/br/zstd
|
|
72
|
+
// bodies are decoded before consumers parse them. Required on Node 24+:
|
|
73
|
+
// attaching any custom dispatcher to the global `fetch` strips the
|
|
74
|
+
// `content-encoding` header but does not actually decompress the body,
|
|
75
|
+
// so callers receive raw gzipped bytes and `JSON.parse` fails.
|
|
76
|
+
// See https://github.com/Doist/todoist-cli/issues/318.
|
|
77
|
+
const decompress = suppressExperimentalWarningsSync(() => interceptors.decompress());
|
|
78
|
+
return new EnvHttpProxyAgent(KEEP_ALIVE_OPTIONS).compose(decompress);
|
|
79
|
+
}
|
|
80
|
+
// undici emits an `ExperimentalWarning` the first time `interceptors.decompress()`
|
|
81
|
+
// runs. The interceptor is stable for our gzipped-JSON-over-HTTPS use case;
|
|
82
|
+
// silence the warning during dispatcher init only so it does not leak to every
|
|
83
|
+
// consumer's stderr on the first request.
|
|
84
|
+
//
|
|
85
|
+
// `fn` must be synchronous so the override covers a single critical section
|
|
86
|
+
// (microseconds) — no unrelated `ExperimentalWarning` from elsewhere can
|
|
87
|
+
// interleave and be lost. We suppress every `ExperimentalWarning` rather than
|
|
88
|
+
// pattern-matching the message text: the message wording is an undici
|
|
89
|
+
// implementation detail (not a stable API), and the suppression window is
|
|
90
|
+
// narrow enough that a coarse type filter is safe.
|
|
91
|
+
//
|
|
92
|
+
// Exported for direct unit testing — the integration path through
|
|
93
|
+
// `getDefaultDispatcher()` cannot reliably exercise the helper because both
|
|
94
|
+
// the dispatcher singleton and undici's internal `warningEmitted` flag are
|
|
95
|
+
// once-per-process.
|
|
96
|
+
function suppressExperimentalWarningsSync(fn) {
|
|
97
|
+
const originalEmit = process.emitWarning;
|
|
98
|
+
process.emitWarning = ((warning, typeOrOptions, ...rest) => {
|
|
99
|
+
const type = typeof typeOrOptions === 'string'
|
|
100
|
+
? typeOrOptions
|
|
101
|
+
: typeof typeOrOptions === 'object' && typeOrOptions !== null
|
|
102
|
+
? typeOrOptions.type
|
|
103
|
+
: undefined;
|
|
104
|
+
if (type === 'ExperimentalWarning')
|
|
105
|
+
return;
|
|
106
|
+
originalEmit.call(process, warning, typeOrOptions, ...rest);
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
return fn();
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
process.emitWarning = originalEmit;
|
|
113
|
+
}
|
|
68
114
|
}
|
|
69
115
|
function isNodeEnvironment() {
|
|
70
116
|
var _a;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.wrapAsFileResponse = wrapAsFileResponse;
|
|
4
|
+
const headers_1 = require("./headers");
|
|
5
|
+
/**
|
|
6
|
+
* Wraps either a native `Response` or a `CustomFetchResponse` into the SDK's
|
|
7
|
+
* `FileResponse` shape (binary endpoints: `viewAttachment`, `downloadBackup`).
|
|
8
|
+
*
|
|
9
|
+
* `arrayBuffer()` is resolved lazily. A customFetch implementation that omits
|
|
10
|
+
* `arrayBuffer()` is still valid for callers that only read `text()`/`json()`;
|
|
11
|
+
* the descriptive error is thrown only if the caller actually requests binary
|
|
12
|
+
* bytes. This avoids the lossy `text()` → `TextEncoder` round-trip that
|
|
13
|
+
* corrupts non-UTF-8 bytes (every `0x80`-`0xFF` byte not part of a valid UTF-8
|
|
14
|
+
* sequence is replaced with `U+FFFD`).
|
|
15
|
+
*/
|
|
16
|
+
function wrapAsFileResponse(response, context) {
|
|
17
|
+
const headers = response.headers instanceof Headers ? (0, headers_1.headersToRecord)(response.headers) : response.headers;
|
|
18
|
+
return {
|
|
19
|
+
ok: response.ok,
|
|
20
|
+
status: response.status,
|
|
21
|
+
statusText: response.statusText,
|
|
22
|
+
headers,
|
|
23
|
+
text: () => response.text(),
|
|
24
|
+
json: () => response.json(),
|
|
25
|
+
arrayBuffer: () => {
|
|
26
|
+
if (typeof response.arrayBuffer !== 'function') {
|
|
27
|
+
throw new Error(`customFetch response must implement arrayBuffer() for ${context} (binary endpoint); reading the body via text() corrupts non-UTF-8 bytes`);
|
|
28
|
+
}
|
|
29
|
+
return response.arrayBuffer();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.uploadMultipartFile = uploadMultipartFile;
|
|
37
37
|
const fetch_with_retry_1 = require("../transport/fetch-with-retry");
|
|
38
|
+
const case_conversion_1 = require("./case-conversion");
|
|
38
39
|
/**
|
|
39
40
|
* Helper function to determine content-type from filename extension.
|
|
40
41
|
* @param fileName - The filename to analyze
|
|
@@ -174,5 +175,5 @@ async function uploadMultipartFile(args) {
|
|
|
174
175
|
},
|
|
175
176
|
customFetch,
|
|
176
177
|
});
|
|
177
|
-
return response.data;
|
|
178
|
+
return (0, case_conversion_1.camelCaseKeys)(response.data);
|
|
178
179
|
}
|
|
@@ -13,7 +13,6 @@ import { z } from 'zod';
|
|
|
13
13
|
import { ENDPOINT_REST_APPS, ENDPOINT_REST_APPS_INSTALLATIONS, ENDPOINT_REST_USER_AUTHORIZATIONS, ENDPOINT_REST_USER_AUTHORIZATIONS_DELETE, getAppByDistributionTokenEndpoint, getAppClientSecretEndpoint, getAppDistributionTokenEndpoint, getAppEndpoint, getAppIconEndpoint, getAppInstallationEndpoint, getAppSecretsEndpoint, getAppTestTokenEndpoint, getAppTokensEndpoint, getAppVerificationTokenEndpoint, getAppWebhookEndpoint, } from '../consts/endpoints.js';
|
|
14
14
|
import { isSuccess, request } from '../transport/http-client.js';
|
|
15
15
|
import { webhookEventToWireName } from '../types/apps/index.js';
|
|
16
|
-
import { camelCaseKeys } from '../utils/case-conversion.js';
|
|
17
16
|
import { uploadMultipartFile } from '../utils/multipart-upload.js';
|
|
18
17
|
import { IdSchema, validateApp, validateAppArray, validateAppByDistributionToken, validateAppDistributionToken, validateAppInstallation, validateAppInstallationArray, validateAppSecrets, validateAppTestToken, validateAppVerificationToken, validateAppWebhook, validateAppWithUserCount, validateUserAuthorizationArray, } from '../utils/validators.js';
|
|
19
18
|
import { BaseClient } from './base-client.js';
|
|
@@ -142,7 +141,7 @@ export class AppClient extends BaseClient {
|
|
|
142
141
|
requestId: requestId,
|
|
143
142
|
customFetch: this.customFetch,
|
|
144
143
|
});
|
|
145
|
-
return validateAppWithUserCount(
|
|
144
|
+
return validateAppWithUserCount(data);
|
|
146
145
|
}
|
|
147
146
|
async getAppTestToken(appId, requestId) {
|
|
148
147
|
IdSchema.parse(appId);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ENDPOINT_REST_BACKUPS, ENDPOINT_REST_BACKUPS_DOWNLOAD } from '../consts/endpoints.js';
|
|
2
2
|
import { request } from '../transport/http-client.js';
|
|
3
|
-
import {
|
|
3
|
+
import { wrapAsFileResponse } from '../utils/file-response.js';
|
|
4
4
|
import { validateBackupArray } from '../utils/validators.js';
|
|
5
5
|
import { BaseClient } from './base-client.js';
|
|
6
6
|
/**
|
|
@@ -32,30 +32,12 @@ export class BackupClient extends BaseClient {
|
|
|
32
32
|
if (!response.ok) {
|
|
33
33
|
throw new Error(`Failed to download backup: ${response.status} ${response.statusText}`);
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
const buffer = new TextEncoder().encode(text).buffer;
|
|
37
|
-
return {
|
|
38
|
-
ok: response.ok,
|
|
39
|
-
status: response.status,
|
|
40
|
-
statusText: response.statusText,
|
|
41
|
-
headers: response.headers,
|
|
42
|
-
text: () => Promise.resolve(text),
|
|
43
|
-
json: () => response.json(),
|
|
44
|
-
arrayBuffer: () => Promise.resolve(buffer),
|
|
45
|
-
};
|
|
35
|
+
return wrapAsFileResponse(response, 'downloadBackup');
|
|
46
36
|
}
|
|
47
37
|
const response = await fetch(url, fetchOptions);
|
|
48
38
|
if (!response.ok) {
|
|
49
39
|
throw new Error(`Failed to download backup: ${response.status} ${response.statusText}`);
|
|
50
40
|
}
|
|
51
|
-
return
|
|
52
|
-
ok: response.ok,
|
|
53
|
-
status: response.status,
|
|
54
|
-
statusText: response.statusText,
|
|
55
|
-
headers: headersToRecord(response.headers),
|
|
56
|
-
text: () => response.text(),
|
|
57
|
-
json: () => response.json(),
|
|
58
|
-
arrayBuffer: () => response.arrayBuffer(),
|
|
59
|
-
};
|
|
41
|
+
return wrapAsFileResponse(response, 'downloadBackup');
|
|
60
42
|
}
|
|
61
43
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ENDPOINT_REST_TEMPLATES_CREATE_FROM_FILE, ENDPOINT_REST_TEMPLATES_FILE, ENDPOINT_REST_TEMPLATES_IMPORT_FROM_FILE, ENDPOINT_REST_TEMPLATES_IMPORT_FROM_ID, ENDPOINT_REST_TEMPLATES_URL, } from '../consts/endpoints.js';
|
|
2
2
|
import { request } from '../transport/http-client.js';
|
|
3
|
-
import { camelCaseKeys } from '../utils/case-conversion.js';
|
|
4
3
|
import { uploadMultipartFile } from '../utils/multipart-upload.js';
|
|
5
4
|
import { validateCommentArray, validateProjectArray, validateSectionArray, validateTaskArray, } from '../utils/validators.js';
|
|
6
5
|
import { BaseClient } from './base-client.js';
|
|
@@ -50,7 +49,7 @@ export class TemplateClient extends BaseClient {
|
|
|
50
49
|
customFetch: this.customFetch,
|
|
51
50
|
requestId,
|
|
52
51
|
});
|
|
53
|
-
return this.validateTemplateResponse(
|
|
52
|
+
return this.validateTemplateResponse(data);
|
|
54
53
|
}
|
|
55
54
|
async importTemplateIntoProject(args, requestId) {
|
|
56
55
|
const { file, fileName, projectId } = args;
|
|
@@ -64,7 +63,7 @@ export class TemplateClient extends BaseClient {
|
|
|
64
63
|
customFetch: this.customFetch,
|
|
65
64
|
requestId,
|
|
66
65
|
});
|
|
67
|
-
return this.validateTemplateResponse(
|
|
66
|
+
return this.validateTemplateResponse(data);
|
|
68
67
|
}
|
|
69
68
|
async importTemplateFromId(args, requestId) {
|
|
70
69
|
const { data } = await request({
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ENDPOINT_REST_APPS_UI_EXTENSIONS, ENDPOINT_REST_APPS_UI_EXTENSIONS_INSTALLED, getUiExtensionEndpoint, getUiExtensionIconEndpoint, getUiExtensionsByIntegrationEndpoint, } from '../consts/endpoints.js';
|
|
2
2
|
import { isSuccess, request } from '../transport/http-client.js';
|
|
3
|
-
import { camelCaseKeys } from '../utils/case-conversion.js';
|
|
4
3
|
import { uploadMultipartFile } from '../utils/multipart-upload.js';
|
|
5
4
|
import { IdSchema, validateUiExtension, validateUiExtensionArray } from '../utils/validators.js';
|
|
6
5
|
import { BaseClient } from './base-client.js';
|
|
@@ -99,6 +98,6 @@ export class UiExtensionClient extends BaseClient {
|
|
|
99
98
|
requestId: requestId,
|
|
100
99
|
customFetch: this.customFetch,
|
|
101
100
|
});
|
|
102
|
-
return validateUiExtension(
|
|
101
|
+
return validateUiExtension(data);
|
|
103
102
|
}
|
|
104
103
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ENDPOINT_REST_UPLOADS } from '../consts/endpoints.js';
|
|
2
2
|
import { isSuccess, request } from '../transport/http-client.js';
|
|
3
|
-
import {
|
|
3
|
+
import { wrapAsFileResponse } from '../utils/file-response.js';
|
|
4
4
|
import { uploadMultipartFile } from '../utils/multipart-upload.js';
|
|
5
5
|
import { validateAttachment } from '../utils/validators.js';
|
|
6
6
|
import { BaseClient } from './base-client.js';
|
|
@@ -68,31 +68,12 @@ export class UploadClient extends BaseClient {
|
|
|
68
68
|
if (!response.ok) {
|
|
69
69
|
throw new Error(`Failed to fetch attachment: ${response.status} ${response.statusText}`);
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
const text = await response.text();
|
|
73
|
-
const buffer = new TextEncoder().encode(text).buffer;
|
|
74
|
-
return {
|
|
75
|
-
ok: response.ok,
|
|
76
|
-
status: response.status,
|
|
77
|
-
statusText: response.statusText,
|
|
78
|
-
headers: response.headers,
|
|
79
|
-
text: () => Promise.resolve(text),
|
|
80
|
-
json: () => response.json(),
|
|
81
|
-
arrayBuffer: () => Promise.resolve(buffer),
|
|
82
|
-
};
|
|
71
|
+
return wrapAsFileResponse(response, 'viewAttachment');
|
|
83
72
|
}
|
|
84
73
|
const response = await fetch(fileUrl, fetchOptions);
|
|
85
74
|
if (!response.ok) {
|
|
86
75
|
throw new Error(`Failed to fetch attachment: ${response.status} ${response.statusText}`);
|
|
87
76
|
}
|
|
88
|
-
return
|
|
89
|
-
ok: response.ok,
|
|
90
|
-
status: response.status,
|
|
91
|
-
statusText: response.statusText,
|
|
92
|
-
headers: headersToRecord(response.headers),
|
|
93
|
-
text: () => response.text(),
|
|
94
|
-
json: () => response.json(),
|
|
95
|
-
arrayBuffer: () => response.arrayBuffer(),
|
|
96
|
-
};
|
|
77
|
+
return wrapAsFileResponse(response, 'viewAttachment');
|
|
97
78
|
}
|
|
98
79
|
}
|
|
@@ -45,6 +45,7 @@ export function createObsidianFetchAdapter(requestUrl) {
|
|
|
45
45
|
// Wrap Obsidian's direct properties as methods returning promises
|
|
46
46
|
text: () => Promise.resolve(response.text),
|
|
47
47
|
json: () => Promise.resolve(response.json),
|
|
48
|
+
arrayBuffer: () => Promise.resolve(response.arrayBuffer),
|
|
48
49
|
};
|
|
49
50
|
};
|
|
50
51
|
}
|
|
@@ -26,8 +26,53 @@ export async function resetDefaultDispatcherForTests() {
|
|
|
26
26
|
await dispatcherPromise.then((dispatcher) => dispatcher.close(), () => undefined);
|
|
27
27
|
}
|
|
28
28
|
async function createDefaultDispatcher() {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// Dynamic import so non-Node consumers (browser, edge runtimes) don't pull
|
|
30
|
+
// undici into their bundle. `getDefaultDispatcher` already short-circuits
|
|
31
|
+
// outside Node, so this branch only runs when undici is safe to load.
|
|
32
|
+
const { EnvHttpProxyAgent, interceptors } = await import('undici');
|
|
33
|
+
// Compose the response-decompression interceptor so gzip/deflate/br/zstd
|
|
34
|
+
// bodies are decoded before consumers parse them. Required on Node 24+:
|
|
35
|
+
// attaching any custom dispatcher to the global `fetch` strips the
|
|
36
|
+
// `content-encoding` header but does not actually decompress the body,
|
|
37
|
+
// so callers receive raw gzipped bytes and `JSON.parse` fails.
|
|
38
|
+
// See https://github.com/Doist/todoist-cli/issues/318.
|
|
39
|
+
const decompress = suppressExperimentalWarningsSync(() => interceptors.decompress());
|
|
40
|
+
return new EnvHttpProxyAgent(KEEP_ALIVE_OPTIONS).compose(decompress);
|
|
41
|
+
}
|
|
42
|
+
// undici emits an `ExperimentalWarning` the first time `interceptors.decompress()`
|
|
43
|
+
// runs. The interceptor is stable for our gzipped-JSON-over-HTTPS use case;
|
|
44
|
+
// silence the warning during dispatcher init only so it does not leak to every
|
|
45
|
+
// consumer's stderr on the first request.
|
|
46
|
+
//
|
|
47
|
+
// `fn` must be synchronous so the override covers a single critical section
|
|
48
|
+
// (microseconds) — no unrelated `ExperimentalWarning` from elsewhere can
|
|
49
|
+
// interleave and be lost. We suppress every `ExperimentalWarning` rather than
|
|
50
|
+
// pattern-matching the message text: the message wording is an undici
|
|
51
|
+
// implementation detail (not a stable API), and the suppression window is
|
|
52
|
+
// narrow enough that a coarse type filter is safe.
|
|
53
|
+
//
|
|
54
|
+
// Exported for direct unit testing — the integration path through
|
|
55
|
+
// `getDefaultDispatcher()` cannot reliably exercise the helper because both
|
|
56
|
+
// the dispatcher singleton and undici's internal `warningEmitted` flag are
|
|
57
|
+
// once-per-process.
|
|
58
|
+
export function suppressExperimentalWarningsSync(fn) {
|
|
59
|
+
const originalEmit = process.emitWarning;
|
|
60
|
+
process.emitWarning = ((warning, typeOrOptions, ...rest) => {
|
|
61
|
+
const type = typeof typeOrOptions === 'string'
|
|
62
|
+
? typeOrOptions
|
|
63
|
+
: typeof typeOrOptions === 'object' && typeOrOptions !== null
|
|
64
|
+
? typeOrOptions.type
|
|
65
|
+
: undefined;
|
|
66
|
+
if (type === 'ExperimentalWarning')
|
|
67
|
+
return;
|
|
68
|
+
originalEmit.call(process, warning, typeOrOptions, ...rest);
|
|
69
|
+
});
|
|
70
|
+
try {
|
|
71
|
+
return fn();
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
process.emitWarning = originalEmit;
|
|
75
|
+
}
|
|
31
76
|
}
|
|
32
77
|
function isNodeEnvironment() {
|
|
33
78
|
var _a;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { headersToRecord } from './headers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wraps either a native `Response` or a `CustomFetchResponse` into the SDK's
|
|
4
|
+
* `FileResponse` shape (binary endpoints: `viewAttachment`, `downloadBackup`).
|
|
5
|
+
*
|
|
6
|
+
* `arrayBuffer()` is resolved lazily. A customFetch implementation that omits
|
|
7
|
+
* `arrayBuffer()` is still valid for callers that only read `text()`/`json()`;
|
|
8
|
+
* the descriptive error is thrown only if the caller actually requests binary
|
|
9
|
+
* bytes. This avoids the lossy `text()` → `TextEncoder` round-trip that
|
|
10
|
+
* corrupts non-UTF-8 bytes (every `0x80`-`0xFF` byte not part of a valid UTF-8
|
|
11
|
+
* sequence is replaced with `U+FFFD`).
|
|
12
|
+
*/
|
|
13
|
+
export function wrapAsFileResponse(response, context) {
|
|
14
|
+
const headers = response.headers instanceof Headers ? headersToRecord(response.headers) : response.headers;
|
|
15
|
+
return {
|
|
16
|
+
ok: response.ok,
|
|
17
|
+
status: response.status,
|
|
18
|
+
statusText: response.statusText,
|
|
19
|
+
headers,
|
|
20
|
+
text: () => response.text(),
|
|
21
|
+
json: () => response.json(),
|
|
22
|
+
arrayBuffer: () => {
|
|
23
|
+
if (typeof response.arrayBuffer !== 'function') {
|
|
24
|
+
throw new Error(`customFetch response must implement arrayBuffer() for ${context} (binary endpoint); reading the body via text() corrupts non-UTF-8 bytes`);
|
|
25
|
+
}
|
|
26
|
+
return response.arrayBuffer();
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fetchWithRetry } from '../transport/fetch-with-retry.js';
|
|
2
|
+
import { camelCaseKeys } from './case-conversion.js';
|
|
2
3
|
/**
|
|
3
4
|
* Helper function to determine content-type from filename extension.
|
|
4
5
|
* @param fileName - The filename to analyze
|
|
@@ -138,5 +139,5 @@ export async function uploadMultipartFile(args) {
|
|
|
138
139
|
},
|
|
139
140
|
customFetch,
|
|
140
141
|
});
|
|
141
|
-
return response.data;
|
|
142
|
+
return camelCaseKeys(response.data);
|
|
142
143
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { Dispatcher } from 'undici';
|
|
2
2
|
export declare function getDefaultDispatcher(): Promise<Dispatcher | undefined>;
|
|
3
3
|
export declare function resetDefaultDispatcherForTests(): Promise<void>;
|
|
4
|
+
export declare function suppressExperimentalWarningsSync<T>(fn: () => T): T;
|
|
@@ -67,7 +67,12 @@ export declare function isNetworkError(error: Error): error is NetworkError;
|
|
|
67
67
|
*/
|
|
68
68
|
export declare function isHttpError(error: Error): error is HttpError;
|
|
69
69
|
/**
|
|
70
|
-
* Custom fetch response interface that custom HTTP clients must implement
|
|
70
|
+
* Custom fetch response interface that custom HTTP clients must implement.
|
|
71
|
+
*
|
|
72
|
+
* `arrayBuffer()` is optional for back-compat with existing implementations,
|
|
73
|
+
* but must be provided when the fetch is used with binary endpoints
|
|
74
|
+
* (`viewAttachment`, `downloadBackup`) — those endpoints throw at runtime if
|
|
75
|
+
* it is missing rather than fall back to a lossy `text()` round-trip.
|
|
71
76
|
*/
|
|
72
77
|
export type CustomFetchResponse = {
|
|
73
78
|
ok: boolean;
|
|
@@ -76,6 +81,7 @@ export type CustomFetchResponse = {
|
|
|
76
81
|
headers: Record<string, string>;
|
|
77
82
|
text(): Promise<string>;
|
|
78
83
|
json(): Promise<unknown>;
|
|
84
|
+
arrayBuffer?: () => Promise<ArrayBuffer>;
|
|
79
85
|
};
|
|
80
86
|
/**
|
|
81
87
|
* Custom fetch function type for alternative HTTP clients
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CustomFetchResponse, FileResponse } from '../types/http.js';
|
|
2
|
+
type AnyResponse = CustomFetchResponse | Response;
|
|
3
|
+
/**
|
|
4
|
+
* Wraps either a native `Response` or a `CustomFetchResponse` into the SDK's
|
|
5
|
+
* `FileResponse` shape (binary endpoints: `viewAttachment`, `downloadBackup`).
|
|
6
|
+
*
|
|
7
|
+
* `arrayBuffer()` is resolved lazily. A customFetch implementation that omits
|
|
8
|
+
* `arrayBuffer()` is still valid for callers that only read `text()`/`json()`;
|
|
9
|
+
* the descriptive error is thrown only if the caller actually requests binary
|
|
10
|
+
* bytes. This avoids the lossy `text()` → `TextEncoder` round-trip that
|
|
11
|
+
* corrupts non-UTF-8 bytes (every `0x80`-`0xFF` byte not part of a valid UTF-8
|
|
12
|
+
* sequence is replaced with `U+FFFD`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function wrapAsFileResponse(response: AnyResponse, context: 'viewAttachment' | 'downloadBackup'): FileResponse;
|
|
15
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doist/todoist-sdk",
|
|
3
|
-
"version": "10.2.0-next.
|
|
3
|
+
"version": "10.2.0-next.3",
|
|
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",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"form-data": "4.0.5",
|
|
53
53
|
"ts-custom-error": "^3.2.0",
|
|
54
54
|
"undici": "^7.16.0",
|
|
55
|
-
"uuid": "11.1.
|
|
55
|
+
"uuid": "11.1.1",
|
|
56
56
|
"zod": "4.3.6"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|