@doist/todoist-sdk 8.0.1 → 8.2.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 +23 -24
- package/dist/cjs/authentication.js +76 -9
- package/dist/cjs/consts/endpoints.js +2 -1
- package/dist/cjs/test-utils/msw-setup.js +2 -2
- package/dist/cjs/todoist-api.js +14 -9
- package/dist/cjs/types/reminders/requests.js +1 -1
- package/dist/cjs/types/sync/commands/goals.js +2 -0
- package/dist/cjs/types/sync/commands/index.js +1 -0
- package/dist/cjs/types/sync/resource-types.js +1 -0
- package/dist/cjs/types/sync/resources/goals.js +33 -0
- package/dist/cjs/types/sync/resources/index.js +1 -0
- package/dist/cjs/types/tasks/types.js +1 -1
- package/dist/cjs/utils/validators.js +36 -36
- package/dist/esm/authentication.js +74 -8
- package/dist/esm/consts/endpoints.js +1 -0
- package/dist/esm/test-utils/msw-setup.js +2 -2
- package/dist/esm/todoist-api.js +14 -9
- package/dist/esm/types/reminders/requests.js +1 -1
- package/dist/esm/types/sync/commands/goals.js +1 -0
- package/dist/esm/types/sync/commands/index.js +1 -0
- package/dist/esm/types/sync/resource-types.js +1 -0
- package/dist/esm/types/sync/resources/goals.js +30 -0
- package/dist/esm/types/sync/resources/index.js +1 -0
- package/dist/esm/types/tasks/types.js +1 -1
- package/dist/esm/utils/validators.js +8 -8
- package/dist/types/authentication.d.ts +73 -5
- package/dist/types/consts/endpoints.d.ts +1 -0
- package/dist/types/todoist-api.d.ts +16 -16
- package/dist/types/types/projects/requests.d.ts +1 -1
- package/dist/types/types/sync/commands/goals.d.ts +33 -0
- package/dist/types/types/sync/commands/index.d.ts +21 -12
- package/dist/types/types/sync/commands/projects.d.ts +1 -1
- package/dist/types/types/sync/commands/reminders.d.ts +1 -1
- package/dist/types/types/sync/resource-types.d.ts +1 -1
- package/dist/types/types/sync/resources/goals.d.ts +37 -0
- package/dist/types/types/sync/resources/index.d.ts +1 -0
- package/dist/types/types/sync/resources/view-options.d.ts +9 -9
- package/dist/types/types/sync/response.d.ts +4 -3
- package/dist/types/utils/validators.d.ts +8 -8
- package/package.json +6 -11
package/README.md
CHANGED
|
@@ -34,21 +34,21 @@ If you're migrating from an older version of the Todoist API (v9), please refer
|
|
|
34
34
|
|
|
35
35
|
Key changes in v1 include:
|
|
36
36
|
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
37
|
+
- Updated endpoint structure
|
|
38
|
+
- New pagination system
|
|
39
|
+
- Unified error response format
|
|
40
|
+
- Object renames (e.g., items → tasks, notes → comments)
|
|
41
|
+
- URL renames and endpoint signature changes
|
|
42
42
|
|
|
43
43
|
## Custom HTTP Clients
|
|
44
44
|
|
|
45
45
|
The Todoist API client supports custom HTTP implementations to enable usage in environments with specific networking requirements, such as:
|
|
46
46
|
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
47
|
+
- **Obsidian plugins** - Desktop app with strict CORS policies
|
|
48
|
+
- **Browser extensions** - Custom HTTP APIs with different security models
|
|
49
|
+
- **Electron apps** - Requests routed through IPC layer
|
|
50
|
+
- **React Native** - Different networking stack
|
|
51
|
+
- **Enterprise environments** - Proxy configuration, custom headers, or certificate handling
|
|
52
52
|
|
|
53
53
|
### Basic Usage
|
|
54
54
|
|
|
@@ -106,20 +106,20 @@ const { accessToken } = await getAuthToken(args, baseUrl)
|
|
|
106
106
|
|
|
107
107
|
### Important Notes
|
|
108
108
|
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
109
|
+
- All existing transforms (snake_case ↔ camelCase) work automatically with custom fetch
|
|
110
|
+
- Retry logic and error handling are preserved
|
|
111
|
+
- File uploads work with custom fetch implementations
|
|
112
|
+
- The custom fetch function should handle FormData for multipart uploads
|
|
113
|
+
- Timeout parameter is optional and up to your custom implementation
|
|
114
114
|
|
|
115
115
|
## Development and Testing
|
|
116
116
|
|
|
117
117
|
Instead of having an example app in the repository to assist development and testing, we have included [ts-node](https://github.com/TypeStrong/ts-node) as a dev dependency. This allows us to have a scratch file locally that can import and utilize the API while developing or reviewing pull requests without having to manage a separate app project.
|
|
118
118
|
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
-
|
|
119
|
+
- `npm install`
|
|
120
|
+
- Add a file named `scratch.ts` in the `src` folder.
|
|
121
|
+
- Configure your IDE to run the scratch file with `ts-node` (instructions for [VSCode](https://medium.com/@dupski/debug-typescript-in-vs-code-without-compiling-using-ts-node-9d1f4f9a94a), [WebStorm](https://www.jetbrains.com/help/webstorm/running-and-debugging-typescript.html#ws_ts_run_debug_server_side_ts_node)), or you can optionally run ts-node in a terminal using instructions [here](https://github.com/TypeStrong/ts-node) (`npx ts-node ./src/scratch.ts` should be enough).
|
|
122
|
+
- Import and call the relevant modules and run the scratch file.
|
|
123
123
|
|
|
124
124
|
Example scratch.ts file:
|
|
125
125
|
|
|
@@ -169,10 +169,10 @@ This project uses [Release Please](https://github.com/googleapis/release-please)
|
|
|
169
169
|
|
|
170
170
|
When making changes, use conventional commit messages:
|
|
171
171
|
|
|
172
|
-
-
|
|
173
|
-
-
|
|
174
|
-
-
|
|
175
|
-
-
|
|
172
|
+
- `feat:` - New features (triggers a minor version bump)
|
|
173
|
+
- `fix:` - Bug fixes (triggers a patch version bump)
|
|
174
|
+
- `feat!:` or `BREAKING CHANGE:` - Breaking changes (triggers a major version bump)
|
|
175
|
+
- `chore:`, `docs:`, `refactor:`, `perf:` - Other changes (included in changelog)
|
|
176
176
|
|
|
177
177
|
Example:
|
|
178
178
|
|
|
@@ -187,7 +187,6 @@ feat!: remove deprecated getTask method
|
|
|
187
187
|
The release process is fully automated:
|
|
188
188
|
|
|
189
189
|
1. **Automatic PR Creation**: When commits are merged to `main`, Release Please automatically creates or updates a release PR with:
|
|
190
|
-
|
|
191
190
|
- Updated version in `package.json`
|
|
192
191
|
- Updated `CHANGELOG.md`
|
|
193
192
|
- Aggregated changes since the last release
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
2
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PERMISSIONS = void 0;
|
|
14
|
+
exports.TOKEN_ENDPOINT_AUTH_METHODS = exports.PERMISSIONS = void 0;
|
|
4
15
|
exports.getAuthStateParameter = getAuthStateParameter;
|
|
5
16
|
exports.getAuthorizationUrl = getAuthorizationUrl;
|
|
6
17
|
exports.getAuthToken = getAuthToken;
|
|
7
18
|
exports.revokeToken = revokeToken;
|
|
8
19
|
exports.migratePersonalToken = migratePersonalToken;
|
|
9
|
-
|
|
20
|
+
exports.registerClient = registerClient;
|
|
10
21
|
const uuid_1 = require("uuid");
|
|
11
|
-
const types_1 = require("./types");
|
|
12
22
|
const endpoints_1 = require("./consts/endpoints");
|
|
23
|
+
const http_client_1 = require("./transport/http-client");
|
|
24
|
+
const types_1 = require("./types");
|
|
13
25
|
/** Available OAuth2 permission scopes. */
|
|
14
26
|
exports.PERMISSIONS = [
|
|
15
27
|
'task:add',
|
|
@@ -19,6 +31,12 @@ exports.PERMISSIONS = [
|
|
|
19
31
|
'project:delete',
|
|
20
32
|
'backups:read',
|
|
21
33
|
];
|
|
34
|
+
/** Supported token endpoint authentication methods for dynamic client registration. */
|
|
35
|
+
exports.TOKEN_ENDPOINT_AUTH_METHODS = [
|
|
36
|
+
'client_secret_post',
|
|
37
|
+
'client_secret_basic',
|
|
38
|
+
'none',
|
|
39
|
+
];
|
|
22
40
|
/**
|
|
23
41
|
* Creates a Basic Authentication header value from client credentials.
|
|
24
42
|
* @param clientId - The OAuth client ID
|
|
@@ -48,13 +66,17 @@ function getAuthStateParameter() {
|
|
|
48
66
|
/**
|
|
49
67
|
* Generates the authorization URL for the OAuth2 flow.
|
|
50
68
|
*
|
|
69
|
+
* The `clientId` can be either a traditional client ID string (e.g. from
|
|
70
|
+
* {@link registerClient}) or an HTTPS URL pointing to a client metadata document,
|
|
71
|
+
* as defined in {@link https://drafts.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/ RFC draft-ietf-oauth-client-id-metadata-document}.
|
|
72
|
+
*
|
|
51
73
|
* @example
|
|
52
74
|
* ```typescript
|
|
53
|
-
* const url = getAuthorizationUrl(
|
|
54
|
-
* 'your-client-id',
|
|
55
|
-
* ['data:read', 'task:add'],
|
|
56
|
-
* state
|
|
57
|
-
* )
|
|
75
|
+
* const url = getAuthorizationUrl({
|
|
76
|
+
* clientId: 'your-client-id',
|
|
77
|
+
* permissions: ['data:read', 'task:add'],
|
|
78
|
+
* state,
|
|
79
|
+
* })
|
|
58
80
|
* // Redirect user to url
|
|
59
81
|
* ```
|
|
60
82
|
*
|
|
@@ -66,7 +88,7 @@ function getAuthorizationUrl({ clientId, permissions, state, baseUrl, }) {
|
|
|
66
88
|
throw new Error('At least one scope value should be passed for permissions.');
|
|
67
89
|
}
|
|
68
90
|
const scope = permissions.join(',');
|
|
69
|
-
return `${(0, endpoints_1.getAuthBaseUri)(baseUrl)}${endpoints_1.ENDPOINT_AUTHORIZATION}?client_id=${clientId}&scope=${scope}&state=${state}`;
|
|
91
|
+
return `${(0, endpoints_1.getAuthBaseUri)(baseUrl)}${endpoints_1.ENDPOINT_AUTHORIZATION}?client_id=${encodeURIComponent(clientId)}&scope=${scope}&state=${state}`;
|
|
70
92
|
}
|
|
71
93
|
/**
|
|
72
94
|
* Exchanges an authorization code for an access token.
|
|
@@ -202,3 +224,48 @@ async function migratePersonalToken(args, options) {
|
|
|
202
224
|
throw new types_1.TodoistRequestError('Personal token migration failed.', err.httpStatusCode, err.responseData);
|
|
203
225
|
}
|
|
204
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Registers a new OAuth client via Dynamic Client Registration (RFC 7591).
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```typescript
|
|
232
|
+
* const client = await registerClient({
|
|
233
|
+
* redirectUris: ['https://example.com/callback'],
|
|
234
|
+
* clientName: 'My App',
|
|
235
|
+
* scope: ['data:read_write', 'task:add'],
|
|
236
|
+
* })
|
|
237
|
+
* // Use client.clientId and client.clientSecret for OAuth flows
|
|
238
|
+
* ```
|
|
239
|
+
*
|
|
240
|
+
* @returns The registered client details
|
|
241
|
+
* @throws {@link TodoistRequestError} If the registration fails
|
|
242
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591 RFC 7591}
|
|
243
|
+
*/
|
|
244
|
+
async function registerClient(args, options) {
|
|
245
|
+
var _a, _b;
|
|
246
|
+
const baseUrl = options === null || options === void 0 ? void 0 : options.baseUrl;
|
|
247
|
+
const customFetch = options === null || options === void 0 ? void 0 : options.customFetch;
|
|
248
|
+
try {
|
|
249
|
+
const response = await (0, http_client_1.request)({
|
|
250
|
+
httpMethod: 'POST',
|
|
251
|
+
baseUri: (0, endpoints_1.getAuthBaseUri)(baseUrl),
|
|
252
|
+
relativePath: endpoints_1.ENDPOINT_REGISTER,
|
|
253
|
+
apiToken: undefined,
|
|
254
|
+
payload: Object.assign(Object.assign({}, args), { scope: (_a = args.scope) === null || _a === void 0 ? void 0 : _a.join(' ') }),
|
|
255
|
+
customFetch,
|
|
256
|
+
});
|
|
257
|
+
if (!(0, http_client_1.isSuccess)(response) || !((_b = response.data) === null || _b === void 0 ? void 0 : _b.clientId)) {
|
|
258
|
+
throw new types_1.TodoistRequestError('Dynamic client registration failed.', response.status, response.data);
|
|
259
|
+
}
|
|
260
|
+
const _c = response.data, { clientIdIssuedAt, clientSecretExpiresAt, scope } = _c, rest = __rest(_c, ["clientIdIssuedAt", "clientSecretExpiresAt", "scope"]);
|
|
261
|
+
return Object.assign(Object.assign({}, rest), { scope: scope ? scope.split(' ') : undefined, clientIdIssuedAt: clientIdIssuedAt !== undefined ? new Date(clientIdIssuedAt * 1000) : undefined, clientSecretExpiresAt: clientSecretExpiresAt === undefined
|
|
262
|
+
? undefined
|
|
263
|
+
: clientSecretExpiresAt === 0
|
|
264
|
+
? null
|
|
265
|
+
: new Date(clientSecretExpiresAt * 1000) });
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
const err = error;
|
|
269
|
+
throw new types_1.TodoistRequestError('Dynamic client registration failed.', err.httpStatusCode, err.responseData);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ENDPOINT_REST_ID_MAPPINGS = exports.ENDPOINT_REST_EMAILS = exports.ENDPOINT_REST_BACKUPS_DOWNLOAD = exports.ENDPOINT_REST_BACKUPS = exports.ENDPOINT_REST_ACCESS_TOKENS_MIGRATE = exports.ENDPOINT_REST_TEMPLATES_IMPORT_FROM_ID = exports.ENDPOINT_REST_TEMPLATES_IMPORT_FROM_FILE = exports.ENDPOINT_REST_TEMPLATES_CREATE_FROM_FILE = exports.ENDPOINT_REST_TEMPLATES_URL = exports.ENDPOINT_REST_TEMPLATES_FILE = exports.ENDPOINT_REST_TASKS_COMPLETED = exports.ENDPOINT_REST_PROJECTS_MOVE_TO_PERSONAL = exports.ENDPOINT_REST_PROJECTS_MOVE_TO_WORKSPACE = exports.SECTION_UNARCHIVE = exports.SECTION_ARCHIVE = exports.ENDPOINT_REST_PROJECT_JOIN = exports.ENDPOINT_REST_PROJECT_FULL = exports.ENDPOINT_REST_PROJECTS_PERMISSIONS = exports.ENDPOINT_REST_PROJECTS_ARCHIVED_COUNT = exports.PROJECT_UNARCHIVE = exports.PROJECT_ARCHIVE = exports.ENDPOINT_REST_UPLOADS = exports.ENDPOINT_REST_ACTIVITIES = exports.ENDPOINT_REST_PRODUCTIVITY = exports.ENDPOINT_REST_USER = exports.ENDPOINT_REST_PROJECT_COLLABORATORS = exports.ENDPOINT_REST_PROJECTS_ARCHIVED = exports.ENDPOINT_REST_PROJECTS_SEARCH = exports.ENDPOINT_REST_PROJECTS = exports.ENDPOINT_REST_TASK_MOVE = exports.ENDPOINT_REST_TASK_REOPEN = exports.ENDPOINT_REST_TASK_CLOSE = exports.ENDPOINT_REST_LOCATION_REMINDERS = exports.ENDPOINT_REST_REMINDERS = exports.ENDPOINT_REST_COMMENTS = exports.ENDPOINT_REST_LABELS_SHARED_REMOVE = exports.ENDPOINT_REST_LABELS_SHARED_RENAME = exports.ENDPOINT_REST_LABELS_SHARED = exports.ENDPOINT_REST_LABELS_SEARCH = exports.ENDPOINT_REST_LABELS = exports.ENDPOINT_REST_SECTIONS_SEARCH = exports.ENDPOINT_REST_SECTIONS = exports.ENDPOINT_REST_TASKS_COMPLETED_SEARCH = exports.ENDPOINT_REST_TASKS_COMPLETED_BY_DUE_DATE = exports.ENDPOINT_REST_TASKS_COMPLETED_BY_COMPLETION_DATE = exports.ENDPOINT_REST_TASKS_FILTER = exports.ENDPOINT_REST_TASKS = exports.API_BASE_URI = exports.API_VERSION = exports.TODOIST_WEB_URI = void 0;
|
|
4
|
-
exports.ENDPOINT_WORKSPACE_MEMBERS = exports.ENDPOINT_WORKSPACE_USERS = exports.ENDPOINT_WORKSPACE_PLAN_DETAILS = exports.ENDPOINT_WORKSPACE_LOGO = exports.ENDPOINT_WORKSPACE_JOIN = exports.ENDPOINT_WORKSPACE_INVITATIONS_DELETE = exports.ENDPOINT_WORKSPACE_INVITATIONS_ALL = exports.ENDPOINT_WORKSPACE_INVITATIONS = exports.ENDPOINT_REST_WORKSPACES = exports.ENDPOINT_REST_FOLDERS = exports.ENDPOINT_REVOKE = exports.ENDPOINT_GET_TOKEN = exports.ENDPOINT_AUTHORIZATION = exports.ENDPOINT_SYNC = exports.ENDPOINT_SYNC_QUICK_ADD = exports.ENDPOINT_REST_MOVED_IDS = void 0;
|
|
4
|
+
exports.ENDPOINT_WORKSPACE_MEMBERS = exports.ENDPOINT_WORKSPACE_USERS = exports.ENDPOINT_WORKSPACE_PLAN_DETAILS = exports.ENDPOINT_WORKSPACE_LOGO = exports.ENDPOINT_WORKSPACE_JOIN = exports.ENDPOINT_WORKSPACE_INVITATIONS_DELETE = exports.ENDPOINT_WORKSPACE_INVITATIONS_ALL = exports.ENDPOINT_WORKSPACE_INVITATIONS = exports.ENDPOINT_REST_WORKSPACES = exports.ENDPOINT_REST_FOLDERS = exports.ENDPOINT_REGISTER = exports.ENDPOINT_REVOKE = exports.ENDPOINT_GET_TOKEN = exports.ENDPOINT_AUTHORIZATION = exports.ENDPOINT_SYNC = exports.ENDPOINT_SYNC_QUICK_ADD = exports.ENDPOINT_REST_MOVED_IDS = void 0;
|
|
5
5
|
exports.getSyncBaseUri = getSyncBaseUri;
|
|
6
6
|
exports.getAuthBaseUri = getAuthBaseUri;
|
|
7
7
|
exports.getWorkspaceInvitationAcceptEndpoint = getWorkspaceInvitationAcceptEndpoint;
|
|
@@ -84,6 +84,7 @@ exports.ENDPOINT_SYNC = 'sync';
|
|
|
84
84
|
exports.ENDPOINT_AUTHORIZATION = 'authorize';
|
|
85
85
|
exports.ENDPOINT_GET_TOKEN = 'access_token';
|
|
86
86
|
exports.ENDPOINT_REVOKE = 'revoke';
|
|
87
|
+
exports.ENDPOINT_REGISTER = 'register';
|
|
87
88
|
exports.ENDPOINT_REST_FOLDERS = 'folders';
|
|
88
89
|
// Workspace endpoints
|
|
89
90
|
exports.ENDPOINT_REST_WORKSPACES = 'workspaces';
|
|
@@ -7,11 +7,11 @@ exports.getAllRequests = getAllRequests;
|
|
|
7
7
|
exports.clearCapturedRequests = clearCapturedRequests;
|
|
8
8
|
exports.mockApiResponse = mockApiResponse;
|
|
9
9
|
exports.mockApiError = mockApiError;
|
|
10
|
-
const vitest_1 = require("vitest");
|
|
11
|
-
const node_1 = require("msw/node");
|
|
12
10
|
const msw_1 = require("msw");
|
|
13
11
|
Object.defineProperty(exports, "http", { enumerable: true, get: function () { return msw_1.http; } });
|
|
14
12
|
Object.defineProperty(exports, "HttpResponse", { enumerable: true, get: function () { return msw_1.HttpResponse; } });
|
|
13
|
+
const node_1 = require("msw/node");
|
|
14
|
+
const vitest_1 = require("vitest");
|
|
15
15
|
// Request capture storage
|
|
16
16
|
const capturedRequests = [];
|
|
17
17
|
// Helper function to capture requests
|
package/dist/cjs/todoist-api.js
CHANGED
|
@@ -12,20 +12,21 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
12
12
|
};
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.TodoistApi = void 0;
|
|
15
|
+
const zod_1 = require("zod");
|
|
16
|
+
const endpoints_1 = require("./consts/endpoints");
|
|
17
|
+
const http_client_1 = require("./transport/http-client");
|
|
15
18
|
const projects_1 = require("./types/projects");
|
|
16
19
|
const reminders_1 = require("./types/reminders");
|
|
17
|
-
const
|
|
18
|
-
const endpoints_1 = require("./consts/endpoints");
|
|
19
|
-
const validators_1 = require("./utils/validators");
|
|
20
|
-
const url_helpers_1 = require("./utils/url-helpers");
|
|
21
|
-
const multipart_upload_1 = require("./utils/multipart-upload");
|
|
22
|
-
const case_conversion_1 = require("./utils/case-conversion");
|
|
20
|
+
const goals_1 = require("./types/sync/resources/goals");
|
|
23
21
|
const activity_helpers_1 = require("./utils/activity-helpers");
|
|
22
|
+
const case_conversion_1 = require("./utils/case-conversion");
|
|
23
|
+
const multipart_upload_1 = require("./utils/multipart-upload");
|
|
24
24
|
const uncompletable_helpers_1 = require("./utils/uncompletable-helpers");
|
|
25
|
-
const
|
|
25
|
+
const url_helpers_1 = require("./utils/url-helpers");
|
|
26
|
+
const validators_1 = require("./utils/validators");
|
|
26
27
|
const uuid_1 = require("uuid");
|
|
27
|
-
const sync_1 = require("./types/sync");
|
|
28
28
|
const types_1 = require("./types");
|
|
29
|
+
const sync_1 = require("./types/sync");
|
|
29
30
|
const MAX_COMMAND_COUNT = 100;
|
|
30
31
|
/**
|
|
31
32
|
* Joins path segments using `/` separator.
|
|
@@ -135,7 +136,11 @@ class TodoistApi {
|
|
|
135
136
|
throw new types_1.TodoistRequestError(value.error, value.httpCode, value.errorExtra);
|
|
136
137
|
});
|
|
137
138
|
}
|
|
138
|
-
|
|
139
|
+
const data = response.data;
|
|
140
|
+
if (data.goals) {
|
|
141
|
+
data.goals = data.goals.map((goal) => goals_1.GoalSchema.parse(goal));
|
|
142
|
+
}
|
|
143
|
+
return data;
|
|
139
144
|
}
|
|
140
145
|
/**
|
|
141
146
|
* Executes a raw Sync API request.
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.UpdateLocationReminderArgsSchema = exports.UpdateReminderArgsSchema = exports.UpdateAbsoluteReminderArgsSchema = exports.UpdateRelativeReminderArgsSchema = exports.AddLocationReminderArgsSchema = exports.AddReminderArgsSchema = exports.AddAbsoluteReminderArgsSchema = exports.AddRelativeReminderArgsSchema = exports.ReminderDueDateSchema = exports.ReminderIdSchema = exports.ReminderDeliveryServiceSchema = exports.REMINDER_DELIVERY_SERVICES = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
|
-
const types_1 = require("../tasks/types");
|
|
6
5
|
const reminders_1 = require("../sync/resources/reminders");
|
|
6
|
+
const types_1 = require("../tasks/types");
|
|
7
7
|
/** Available reminder delivery services. */
|
|
8
8
|
exports.REMINDER_DELIVERY_SERVICES = ['email', 'push'];
|
|
9
9
|
exports.ReminderDeliveryServiceSchema = zod_1.z.enum(exports.REMINDER_DELIVERY_SERVICES);
|
|
@@ -27,6 +27,7 @@ __exportStar(require("./workspaces"), exports);
|
|
|
27
27
|
__exportStar(require("./folders"), exports);
|
|
28
28
|
__exportStar(require("./workspace-filters"), exports);
|
|
29
29
|
__exportStar(require("./workspace-goals"), exports);
|
|
30
|
+
__exportStar(require("./goals"), exports);
|
|
30
31
|
__exportStar(require("./notifications"), exports);
|
|
31
32
|
__exportStar(require("./sharing"), exports);
|
|
32
33
|
__exportStar(require("./view-options"), exports);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GoalSchema = exports.GoalProgressSchema = exports.GOAL_OWNER_TYPES = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
/** Available goal owner types. */
|
|
6
|
+
exports.GOAL_OWNER_TYPES = ['USER', 'WORKSPACE'];
|
|
7
|
+
exports.GoalProgressSchema = zod_1.z
|
|
8
|
+
.object({
|
|
9
|
+
totalItemCount: zod_1.z.number().int(),
|
|
10
|
+
completedItemCount: zod_1.z.number().int(),
|
|
11
|
+
percentage: zod_1.z.number(),
|
|
12
|
+
})
|
|
13
|
+
.passthrough();
|
|
14
|
+
exports.GoalSchema = zod_1.z
|
|
15
|
+
.object({
|
|
16
|
+
id: zod_1.z.string(),
|
|
17
|
+
ownerType: zod_1.z.enum(exports.GOAL_OWNER_TYPES),
|
|
18
|
+
ownerId: zod_1.z.string(),
|
|
19
|
+
name: zod_1.z.string(),
|
|
20
|
+
description: zod_1.z.string().nullable(),
|
|
21
|
+
deadline: zod_1.z.string().nullable(),
|
|
22
|
+
parentGoalId: zod_1.z.string().nullable(),
|
|
23
|
+
childOrder: zod_1.z.number().int(),
|
|
24
|
+
isCompleted: zod_1.z.boolean(),
|
|
25
|
+
completedAt: zod_1.z.coerce.date().nullable(),
|
|
26
|
+
responsibleUid: zod_1.z.string().nullable(),
|
|
27
|
+
isDeleted: zod_1.z.boolean(),
|
|
28
|
+
progress: exports.GoalProgressSchema.optional(),
|
|
29
|
+
creatorUid: zod_1.z.string(),
|
|
30
|
+
createdAt: zod_1.z.coerce.date(),
|
|
31
|
+
updatedAt: zod_1.z.coerce.date(),
|
|
32
|
+
})
|
|
33
|
+
.passthrough();
|
|
@@ -21,6 +21,7 @@ __exportStar(require("./tooltips"), exports);
|
|
|
21
21
|
__exportStar(require("./notes"), exports);
|
|
22
22
|
__exportStar(require("./workspace-filters"), exports);
|
|
23
23
|
__exportStar(require("./workspace-goals"), exports);
|
|
24
|
+
__exportStar(require("./goals"), exports);
|
|
24
25
|
__exportStar(require("./calendars"), exports);
|
|
25
26
|
__exportStar(require("./reminders"), exports);
|
|
26
27
|
__exportStar(require("./completed-info"), exports);
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TaskSchema = exports.DeadlineSchema = exports.DurationSchema = exports.DURATION_UNITS = exports.DueDateSchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
|
-
const url_helpers_1 = require("../../utils/url-helpers");
|
|
6
5
|
const uncompletable_helpers_1 = require("../../utils/uncompletable-helpers");
|
|
6
|
+
const url_helpers_1 = require("../../utils/url-helpers");
|
|
7
7
|
exports.DueDateSchema = zod_1.z
|
|
8
8
|
.object({
|
|
9
9
|
isRecurring: zod_1.z.boolean(),
|
|
@@ -7,18 +7,18 @@ exports.isPersonalProject = isPersonalProject;
|
|
|
7
7
|
exports.validateProject = validateProject;
|
|
8
8
|
exports.validateProjectArray = validateProjectArray;
|
|
9
9
|
exports.validateWorkspaceUserArray = validateWorkspaceUserArray;
|
|
10
|
-
const types_1 = require("../types/
|
|
11
|
-
const types_2 = require("../types/
|
|
12
|
-
const types_3 = require("../types/
|
|
13
|
-
const types_4 = require("../types/
|
|
14
|
-
const types_5 = require("../types/
|
|
15
|
-
const types_6 = require("../types/
|
|
10
|
+
const types_1 = require("../types/activity/types");
|
|
11
|
+
const types_2 = require("../types/backups/types");
|
|
12
|
+
const types_3 = require("../types/comments/types");
|
|
13
|
+
const types_4 = require("../types/id-mappings/types");
|
|
14
|
+
const types_5 = require("../types/insights/types");
|
|
15
|
+
const types_6 = require("../types/labels/types");
|
|
16
16
|
const types_7 = require("../types/productivity/types");
|
|
17
|
-
const types_8 = require("../types/
|
|
18
|
-
const types_9 = require("../types/
|
|
19
|
-
const types_10 = require("../types/
|
|
20
|
-
const types_11 = require("../types/
|
|
21
|
-
const types_12 = require("../types/
|
|
17
|
+
const types_8 = require("../types/projects/types");
|
|
18
|
+
const types_9 = require("../types/sections/types");
|
|
19
|
+
const types_10 = require("../types/tasks/types");
|
|
20
|
+
const types_11 = require("../types/users/types");
|
|
21
|
+
const types_12 = require("../types/workspaces/types");
|
|
22
22
|
const resources_1 = require("../types/sync/resources");
|
|
23
23
|
function createValidator(schema) {
|
|
24
24
|
return (input) => schema.parse(input);
|
|
@@ -27,7 +27,7 @@ function createArrayValidator(validateItem) {
|
|
|
27
27
|
return (input) => input.map(validateItem);
|
|
28
28
|
}
|
|
29
29
|
// Entity validators
|
|
30
|
-
exports.validateTask = createValidator(
|
|
30
|
+
exports.validateTask = createValidator(types_10.TaskSchema);
|
|
31
31
|
exports.validateTaskArray = createArrayValidator(exports.validateTask);
|
|
32
32
|
/**
|
|
33
33
|
* Type guard to check if a project is a workspace project.
|
|
@@ -52,53 +52,53 @@ function isPersonalProject(project) {
|
|
|
52
52
|
*/
|
|
53
53
|
function validateProject(input) {
|
|
54
54
|
if ('workspaceId' in input) {
|
|
55
|
-
return
|
|
55
|
+
return types_8.WorkspaceProjectSchema.parse(input);
|
|
56
56
|
}
|
|
57
|
-
return
|
|
57
|
+
return types_8.PersonalProjectSchema.parse(input);
|
|
58
58
|
}
|
|
59
59
|
function validateProjectArray(input) {
|
|
60
60
|
return input.map(validateProject);
|
|
61
61
|
}
|
|
62
|
-
exports.validateSection = createValidator(
|
|
62
|
+
exports.validateSection = createValidator(types_9.SectionSchema);
|
|
63
63
|
exports.validateSectionArray = createArrayValidator(exports.validateSection);
|
|
64
|
-
exports.validateLabel = createValidator(
|
|
64
|
+
exports.validateLabel = createValidator(types_6.LabelSchema);
|
|
65
65
|
exports.validateLabelArray = createArrayValidator(exports.validateLabel);
|
|
66
|
-
exports.validateComment = createValidator(
|
|
66
|
+
exports.validateComment = createValidator(types_3.CommentSchema);
|
|
67
67
|
exports.validateCommentArray = createArrayValidator(exports.validateComment);
|
|
68
|
-
exports.validateUser = createValidator(
|
|
68
|
+
exports.validateUser = createValidator(types_11.UserSchema);
|
|
69
69
|
exports.validateUserArray = createArrayValidator(exports.validateUser);
|
|
70
70
|
exports.validateProductivityStats = createValidator(types_7.ProductivityStatsSchema);
|
|
71
|
-
exports.validateCurrentUser = createValidator(
|
|
72
|
-
exports.validateActivityEvent = createValidator(
|
|
71
|
+
exports.validateCurrentUser = createValidator(types_11.CurrentUserSchema);
|
|
72
|
+
exports.validateActivityEvent = createValidator(types_1.ActivityEventSchema);
|
|
73
73
|
exports.validateActivityEventArray = createArrayValidator(exports.validateActivityEvent);
|
|
74
|
-
exports.validateAttachment = createValidator(
|
|
75
|
-
exports.validateWorkspaceUser = createValidator(
|
|
74
|
+
exports.validateAttachment = createValidator(types_3.AttachmentSchema);
|
|
75
|
+
exports.validateWorkspaceUser = createValidator(types_12.WorkspaceUserSchema);
|
|
76
76
|
function validateWorkspaceUserArray(input) {
|
|
77
77
|
if (!Array.isArray(input)) {
|
|
78
78
|
throw new Error(`Expected array for workspace users, got ${typeof input}`);
|
|
79
79
|
}
|
|
80
80
|
return input.map(exports.validateWorkspaceUser);
|
|
81
81
|
}
|
|
82
|
-
exports.validateWorkspaceInvitation = createValidator(
|
|
82
|
+
exports.validateWorkspaceInvitation = createValidator(types_12.WorkspaceInvitationSchema);
|
|
83
83
|
exports.validateWorkspaceInvitationArray = createArrayValidator(exports.validateWorkspaceInvitation);
|
|
84
|
-
exports.validateWorkspacePlanDetails = createValidator(
|
|
85
|
-
exports.validateJoinWorkspaceResult = createValidator(
|
|
86
|
-
exports.validateWorkspace = createValidator(
|
|
84
|
+
exports.validateWorkspacePlanDetails = createValidator(types_12.WorkspacePlanDetailsSchema);
|
|
85
|
+
exports.validateJoinWorkspaceResult = createValidator(types_12.JoinWorkspaceResultSchema);
|
|
86
|
+
exports.validateWorkspace = createValidator(types_12.WorkspaceSchema);
|
|
87
87
|
exports.validateWorkspaceArray = createArrayValidator(exports.validateWorkspace);
|
|
88
|
-
exports.validateMemberActivityInfo = createValidator(
|
|
88
|
+
exports.validateMemberActivityInfo = createValidator(types_12.MemberActivityInfoSchema);
|
|
89
89
|
exports.validateMemberActivityInfoArray = createArrayValidator(exports.validateMemberActivityInfo);
|
|
90
|
-
exports.validateWorkspaceUserTask = createValidator(
|
|
90
|
+
exports.validateWorkspaceUserTask = createValidator(types_12.WorkspaceUserTaskSchema);
|
|
91
91
|
exports.validateWorkspaceUserTaskArray = createArrayValidator(exports.validateWorkspaceUserTask);
|
|
92
|
-
exports.validateProjectActivityStats = createValidator(
|
|
93
|
-
exports.validateProjectHealth = createValidator(
|
|
94
|
-
exports.validateProjectHealthContext = createValidator(
|
|
95
|
-
exports.validateProjectProgress = createValidator(
|
|
96
|
-
exports.validateWorkspaceInsights = createValidator(
|
|
97
|
-
exports.validateBackup = createValidator(
|
|
92
|
+
exports.validateProjectActivityStats = createValidator(types_5.ProjectActivityStatsSchema);
|
|
93
|
+
exports.validateProjectHealth = createValidator(types_5.ProjectHealthSchema);
|
|
94
|
+
exports.validateProjectHealthContext = createValidator(types_5.ProjectHealthContextSchema);
|
|
95
|
+
exports.validateProjectProgress = createValidator(types_5.ProjectProgressSchema);
|
|
96
|
+
exports.validateWorkspaceInsights = createValidator(types_5.WorkspaceInsightsSchema);
|
|
97
|
+
exports.validateBackup = createValidator(types_2.BackupSchema);
|
|
98
98
|
exports.validateBackupArray = createArrayValidator(exports.validateBackup);
|
|
99
|
-
exports.validateIdMapping = createValidator(
|
|
99
|
+
exports.validateIdMapping = createValidator(types_4.IdMappingSchema);
|
|
100
100
|
exports.validateIdMappingArray = createArrayValidator(exports.validateIdMapping);
|
|
101
|
-
exports.validateMovedId = createValidator(
|
|
101
|
+
exports.validateMovedId = createValidator(types_4.MovedIdSchema);
|
|
102
102
|
exports.validateMovedIdArray = createArrayValidator(exports.validateMovedId);
|
|
103
103
|
// Sync resource validators
|
|
104
104
|
exports.validateFilter = createValidator(resources_1.FilterSchema);
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
2
12
|
import { v4 as uuid } from 'uuid';
|
|
13
|
+
import { getAuthBaseUri, getSyncBaseUri, ENDPOINT_AUTHORIZATION, ENDPOINT_GET_TOKEN, ENDPOINT_REVOKE, ENDPOINT_REGISTER, ENDPOINT_REST_ACCESS_TOKENS_MIGRATE, } from './consts/endpoints.js';
|
|
14
|
+
import { request, isSuccess } from './transport/http-client.js';
|
|
3
15
|
import { TodoistRequestError } from './types/index.js';
|
|
4
|
-
import { getAuthBaseUri, getSyncBaseUri, ENDPOINT_AUTHORIZATION, ENDPOINT_GET_TOKEN, ENDPOINT_REVOKE, ENDPOINT_REST_ACCESS_TOKENS_MIGRATE, } from './consts/endpoints.js';
|
|
5
16
|
/** Available OAuth2 permission scopes. */
|
|
6
17
|
export const PERMISSIONS = [
|
|
7
18
|
'task:add',
|
|
@@ -11,6 +22,12 @@ export const PERMISSIONS = [
|
|
|
11
22
|
'project:delete',
|
|
12
23
|
'backups:read',
|
|
13
24
|
];
|
|
25
|
+
/** Supported token endpoint authentication methods for dynamic client registration. */
|
|
26
|
+
export const TOKEN_ENDPOINT_AUTH_METHODS = [
|
|
27
|
+
'client_secret_post',
|
|
28
|
+
'client_secret_basic',
|
|
29
|
+
'none',
|
|
30
|
+
];
|
|
14
31
|
/**
|
|
15
32
|
* Creates a Basic Authentication header value from client credentials.
|
|
16
33
|
* @param clientId - The OAuth client ID
|
|
@@ -40,13 +57,17 @@ export function getAuthStateParameter() {
|
|
|
40
57
|
/**
|
|
41
58
|
* Generates the authorization URL for the OAuth2 flow.
|
|
42
59
|
*
|
|
60
|
+
* The `clientId` can be either a traditional client ID string (e.g. from
|
|
61
|
+
* {@link registerClient}) or an HTTPS URL pointing to a client metadata document,
|
|
62
|
+
* as defined in {@link https://drafts.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/ RFC draft-ietf-oauth-client-id-metadata-document}.
|
|
63
|
+
*
|
|
43
64
|
* @example
|
|
44
65
|
* ```typescript
|
|
45
|
-
* const url = getAuthorizationUrl(
|
|
46
|
-
* 'your-client-id',
|
|
47
|
-
* ['data:read', 'task:add'],
|
|
48
|
-
* state
|
|
49
|
-
* )
|
|
66
|
+
* const url = getAuthorizationUrl({
|
|
67
|
+
* clientId: 'your-client-id',
|
|
68
|
+
* permissions: ['data:read', 'task:add'],
|
|
69
|
+
* state,
|
|
70
|
+
* })
|
|
50
71
|
* // Redirect user to url
|
|
51
72
|
* ```
|
|
52
73
|
*
|
|
@@ -58,7 +79,7 @@ export function getAuthorizationUrl({ clientId, permissions, state, baseUrl, })
|
|
|
58
79
|
throw new Error('At least one scope value should be passed for permissions.');
|
|
59
80
|
}
|
|
60
81
|
const scope = permissions.join(',');
|
|
61
|
-
return `${getAuthBaseUri(baseUrl)}${ENDPOINT_AUTHORIZATION}?client_id=${clientId}&scope=${scope}&state=${state}`;
|
|
82
|
+
return `${getAuthBaseUri(baseUrl)}${ENDPOINT_AUTHORIZATION}?client_id=${encodeURIComponent(clientId)}&scope=${scope}&state=${state}`;
|
|
62
83
|
}
|
|
63
84
|
/**
|
|
64
85
|
* Exchanges an authorization code for an access token.
|
|
@@ -194,3 +215,48 @@ export async function migratePersonalToken(args, options) {
|
|
|
194
215
|
throw new TodoistRequestError('Personal token migration failed.', err.httpStatusCode, err.responseData);
|
|
195
216
|
}
|
|
196
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Registers a new OAuth client via Dynamic Client Registration (RFC 7591).
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```typescript
|
|
223
|
+
* const client = await registerClient({
|
|
224
|
+
* redirectUris: ['https://example.com/callback'],
|
|
225
|
+
* clientName: 'My App',
|
|
226
|
+
* scope: ['data:read_write', 'task:add'],
|
|
227
|
+
* })
|
|
228
|
+
* // Use client.clientId and client.clientSecret for OAuth flows
|
|
229
|
+
* ```
|
|
230
|
+
*
|
|
231
|
+
* @returns The registered client details
|
|
232
|
+
* @throws {@link TodoistRequestError} If the registration fails
|
|
233
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591 RFC 7591}
|
|
234
|
+
*/
|
|
235
|
+
export async function registerClient(args, options) {
|
|
236
|
+
var _a, _b;
|
|
237
|
+
const baseUrl = options === null || options === void 0 ? void 0 : options.baseUrl;
|
|
238
|
+
const customFetch = options === null || options === void 0 ? void 0 : options.customFetch;
|
|
239
|
+
try {
|
|
240
|
+
const response = await request({
|
|
241
|
+
httpMethod: 'POST',
|
|
242
|
+
baseUri: getAuthBaseUri(baseUrl),
|
|
243
|
+
relativePath: ENDPOINT_REGISTER,
|
|
244
|
+
apiToken: undefined,
|
|
245
|
+
payload: Object.assign(Object.assign({}, args), { scope: (_a = args.scope) === null || _a === void 0 ? void 0 : _a.join(' ') }),
|
|
246
|
+
customFetch,
|
|
247
|
+
});
|
|
248
|
+
if (!isSuccess(response) || !((_b = response.data) === null || _b === void 0 ? void 0 : _b.clientId)) {
|
|
249
|
+
throw new TodoistRequestError('Dynamic client registration failed.', response.status, response.data);
|
|
250
|
+
}
|
|
251
|
+
const _c = response.data, { clientIdIssuedAt, clientSecretExpiresAt, scope } = _c, rest = __rest(_c, ["clientIdIssuedAt", "clientSecretExpiresAt", "scope"]);
|
|
252
|
+
return Object.assign(Object.assign({}, rest), { scope: scope ? scope.split(' ') : undefined, clientIdIssuedAt: clientIdIssuedAt !== undefined ? new Date(clientIdIssuedAt * 1000) : undefined, clientSecretExpiresAt: clientSecretExpiresAt === undefined
|
|
253
|
+
? undefined
|
|
254
|
+
: clientSecretExpiresAt === 0
|
|
255
|
+
? null
|
|
256
|
+
: new Date(clientSecretExpiresAt * 1000) });
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
const err = error;
|
|
260
|
+
throw new TodoistRequestError('Dynamic client registration failed.', err.httpStatusCode, err.responseData);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -65,6 +65,7 @@ export const ENDPOINT_SYNC = 'sync';
|
|
|
65
65
|
export const ENDPOINT_AUTHORIZATION = 'authorize';
|
|
66
66
|
export const ENDPOINT_GET_TOKEN = 'access_token';
|
|
67
67
|
export const ENDPOINT_REVOKE = 'revoke';
|
|
68
|
+
export const ENDPOINT_REGISTER = 'register';
|
|
68
69
|
export const ENDPOINT_REST_FOLDERS = 'folders';
|
|
69
70
|
// Workspace endpoints
|
|
70
71
|
export const ENDPOINT_REST_WORKSPACES = 'workspaces';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { beforeAll, afterEach, afterAll } from 'vitest';
|
|
2
|
-
import { setupServer } from 'msw/node';
|
|
3
1
|
import { http, HttpResponse } from 'msw';
|
|
2
|
+
import { setupServer } from 'msw/node';
|
|
3
|
+
import { beforeAll, afterEach, afterAll } from 'vitest';
|
|
4
4
|
// Request capture storage
|
|
5
5
|
const capturedRequests = [];
|
|
6
6
|
// Helper function to capture requests
|