@doist/comms-sdk 2.0.2 → 3.0.1
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/transport/fetch-with-retry.js +44 -2
- package/dist/esm/types/errors.js +2 -2
- package/dist/esm/utils/url-helpers.js +104 -16
- package/dist/types/types/errors.d.ts +1 -1
- 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 -202
- 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
|
@@ -4,7 +4,8 @@ import { transformTimestamps } from '../utils/timestamp-conversion.js';
|
|
|
4
4
|
import { getDefaultTransport } from './http-dispatcher.js';
|
|
5
5
|
export async function fetchWithRetry(url, options, maxRetries = 3, customFetch) {
|
|
6
6
|
let lastError;
|
|
7
|
-
|
|
7
|
+
let attempt = 0;
|
|
8
|
+
for (; attempt <= maxRetries; attempt++) {
|
|
8
9
|
let clearTimeoutFn;
|
|
9
10
|
try {
|
|
10
11
|
let requestSignal = options.signal || undefined;
|
|
@@ -56,7 +57,48 @@ export async function fetchWithRetry(url, options, maxRetries = 3, customFetch)
|
|
|
56
57
|
if (lastError instanceof CommsRequestError) {
|
|
57
58
|
throw lastError;
|
|
58
59
|
}
|
|
59
|
-
throw new CommsRequestError(lastError
|
|
60
|
+
throw new CommsRequestError(describeTransportFailure(lastError, attempt + 1), undefined, undefined, {
|
|
61
|
+
cause: lastError,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** Causes reported before the rest of the chain is dropped. */
|
|
65
|
+
const CAUSE_CHAIN_LIMIT = 3;
|
|
66
|
+
/**
|
|
67
|
+
* Builds the message for a request that never produced a response.
|
|
68
|
+
*
|
|
69
|
+
* `fetch` reports every transport problem as the same `fetch failed`, and puts
|
|
70
|
+
* the reason a caller actually needs — `ENOTFOUND`, `ECONNRESET`, a TLS failure
|
|
71
|
+
* — in `cause`. Dropping that leaves an unattributable error in the logs, so
|
|
72
|
+
* unwrap the chain into the message and keep the original as `cause`.
|
|
73
|
+
*/
|
|
74
|
+
function describeTransportFailure(error, attempts) {
|
|
75
|
+
const base = error?.message ?? 'Request failed';
|
|
76
|
+
const cause = describeCauseChain(error);
|
|
77
|
+
const tries = attempts > 1 ? ` after ${attempts} attempts` : '';
|
|
78
|
+
return cause ? `${base} (${cause})${tries}` : `${base}${tries}`;
|
|
79
|
+
}
|
|
80
|
+
function describeCauseChain(error) {
|
|
81
|
+
const descriptions = [];
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
let current = error?.cause;
|
|
84
|
+
while (current instanceof Error &&
|
|
85
|
+
!seen.has(current) &&
|
|
86
|
+
descriptions.length < CAUSE_CHAIN_LIMIT) {
|
|
87
|
+
seen.add(current);
|
|
88
|
+
const description = describeCause(current);
|
|
89
|
+
if (description) {
|
|
90
|
+
descriptions.push(description);
|
|
91
|
+
}
|
|
92
|
+
// A failed connect can carry one error per address it tried; the first
|
|
93
|
+
// one is representative and the rest are the same failure repeated.
|
|
94
|
+
const aggregated = current instanceof AggregateError ? current.errors[0] : undefined;
|
|
95
|
+
current = aggregated ?? current.cause;
|
|
96
|
+
}
|
|
97
|
+
return descriptions.length > 0 ? descriptions.join(' <- ') : undefined;
|
|
98
|
+
}
|
|
99
|
+
function describeCause(error) {
|
|
100
|
+
const code = error.code;
|
|
101
|
+
return typeof code === 'string' && code.length > 0 ? `${code}: ${error.message}` : error.message;
|
|
60
102
|
}
|
|
61
103
|
async function fetchWithDefaultTransport(url, options, signal) {
|
|
62
104
|
// Read the dispatcher and its paired `fetch` as one value so they can never
|
package/dist/esm/types/errors.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CustomError } from 'ts-custom-error';
|
|
2
2
|
export class CommsRequestError extends CustomError {
|
|
3
|
-
constructor(message, httpStatusCode, responseData) {
|
|
4
|
-
super(message);
|
|
3
|
+
constructor(message, httpStatusCode, responseData, options) {
|
|
4
|
+
super(message, options);
|
|
5
5
|
this.httpStatusCode = httpStatusCode;
|
|
6
6
|
this.responseData = responseData;
|
|
7
7
|
}
|
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Helper functions for creating Comms permalinks (`https://comms.todoist.com/...`).
|
|
3
|
+
*
|
|
4
|
+
* Paths come from `@doist/sdk-kmp`, the shared implementation of Comms link
|
|
5
|
+
* routes, so they stay in step with the other Comms clients. The path builders
|
|
6
|
+
* are used rather than the URL builders so that `getFullCommsURL` can keep
|
|
7
|
+
* serving an arbitrary base URL.
|
|
3
8
|
*/
|
|
9
|
+
import { commsChannelPath, commsConversationPath, commsInboxThreadCommentPath, commsInboxThreadPath, commsMessagePath, commsSavedThreadCommentPath, commsSavedThreadPath, commsThreadCommentPath, commsThreadPath, parseCommsLink, } from '@doist/sdk-kmp/comms';
|
|
4
10
|
const COMMS_BASE_URL = 'https://comms.todoist.com';
|
|
11
|
+
/**
|
|
12
|
+
* Builds a path, falling back to a locally built path when the link builder
|
|
13
|
+
* rejects one of the given ids. These helpers run while parsing API responses,
|
|
14
|
+
* so building a path must never fail the parse.
|
|
15
|
+
*
|
|
16
|
+
* Every error is caught, not just id rejection. The link builders expose no
|
|
17
|
+
* error type to narrow on, and a path is never important enough to fail a
|
|
18
|
+
* parse over, so any failure degrades to the locally built path.
|
|
19
|
+
*
|
|
20
|
+
* @param build Builds the path.
|
|
21
|
+
* @param fallback Builds the path without the shared link builder.
|
|
22
|
+
* @returns The built path, or the locally built path if an id was rejected.
|
|
23
|
+
*/
|
|
24
|
+
function buildPath(build, fallback) {
|
|
25
|
+
try {
|
|
26
|
+
return build();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return fallback();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
5
32
|
/**
|
|
6
33
|
* Builds a relative Comms URL based on the provided parameters
|
|
7
34
|
* @param params - URL parameters including workspace, channel, conversation, thread, etc.
|
|
@@ -12,32 +39,33 @@ const COMMS_BASE_URL = 'https://comms.todoist.com';
|
|
|
12
39
|
*/
|
|
13
40
|
export function getCommsURL(params) {
|
|
14
41
|
const { workspaceId, channelId, conversationId, threadId, commentId, messageId, userId } = params;
|
|
15
|
-
|
|
42
|
+
const workspace = String(workspaceId);
|
|
16
43
|
if (channelId) {
|
|
17
|
-
|
|
44
|
+
if (threadId && commentId) {
|
|
45
|
+
return buildPath(() => commsThreadCommentPath(workspace, channelId, threadId, commentId), () => `/${workspace}/ch/${channelId}/t/${threadId}/c/${commentId}`);
|
|
46
|
+
}
|
|
18
47
|
if (threadId) {
|
|
19
|
-
|
|
20
|
-
if (commentId) {
|
|
21
|
-
url += `c/${commentId}`;
|
|
22
|
-
}
|
|
48
|
+
return buildPath(() => commsThreadPath(workspace, channelId, threadId), () => `/${workspace}/ch/${channelId}/t/${threadId}/`);
|
|
23
49
|
}
|
|
50
|
+
return buildPath(() => commsChannelPath(workspace, channelId), () => `/${workspace}/ch/${channelId}/`);
|
|
24
51
|
}
|
|
25
|
-
|
|
26
|
-
url += `inbox/t/${threadId}/`;
|
|
52
|
+
if (threadId) {
|
|
27
53
|
if (commentId) {
|
|
28
|
-
|
|
54
|
+
return buildPath(() => commsInboxThreadCommentPath(workspace, threadId, commentId), () => `/${workspace}/inbox/t/${threadId}/c/${commentId}`);
|
|
29
55
|
}
|
|
56
|
+
return buildPath(() => commsInboxThreadPath(workspace, threadId), () => `/${workspace}/inbox/t/${threadId}/`);
|
|
30
57
|
}
|
|
31
|
-
|
|
32
|
-
url += `msg/${conversationId}/`;
|
|
58
|
+
if (conversationId) {
|
|
33
59
|
if (messageId) {
|
|
34
|
-
|
|
60
|
+
return buildPath(() => commsMessagePath(workspace, conversationId, messageId), () => `/${workspace}/msg/${conversationId}/m/${messageId}`);
|
|
35
61
|
}
|
|
62
|
+
return buildPath(() => commsConversationPath(workspace, conversationId), () => `/${workspace}/msg/${conversationId}/`);
|
|
36
63
|
}
|
|
37
|
-
|
|
38
|
-
|
|
64
|
+
// The people view and the bare workspace root have no shared route.
|
|
65
|
+
if (userId) {
|
|
66
|
+
return `/${workspace}/people/u/${userId}`;
|
|
39
67
|
}
|
|
40
|
-
return
|
|
68
|
+
return `/${workspace}/`;
|
|
41
69
|
}
|
|
42
70
|
/**
|
|
43
71
|
* Builds a full Comms URL (with protocol and hostname) based on the provided parameters
|
|
@@ -92,7 +120,13 @@ export function getSavedThreadsRootURL(workspaceId) {
|
|
|
92
120
|
}
|
|
93
121
|
/** Returns the URL for a saved thread. */
|
|
94
122
|
export function getSavedThreadURL(params) {
|
|
95
|
-
|
|
123
|
+
const workspace = String(params.workspaceId);
|
|
124
|
+
return buildPath(() => commsSavedThreadPath(workspace, params.threadId), () => `/${workspace}/saved/t/${params.threadId}`);
|
|
125
|
+
}
|
|
126
|
+
/** Returns the URL for a comment on a saved thread. */
|
|
127
|
+
export function getSavedThreadCommentURL(params) {
|
|
128
|
+
const workspace = String(params.workspaceId);
|
|
129
|
+
return buildPath(() => commsSavedThreadCommentPath(workspace, params.threadId, params.commentId), () => `/${workspace}/saved/t/${params.threadId}/c/${params.commentId}`);
|
|
96
130
|
}
|
|
97
131
|
/** Returns the URL for the search root. */
|
|
98
132
|
export function getSearchRootURL(workspaceId) {
|
|
@@ -112,3 +146,57 @@ export function getSettingsURL(params) {
|
|
|
112
146
|
export function getTeamMembersRootURL(workspaceId) {
|
|
113
147
|
return `/${workspaceId}/people/u`;
|
|
114
148
|
}
|
|
149
|
+
/** The kinds of Comms entity a URL can point at. */
|
|
150
|
+
export const COMMS_LINK_TYPES = [
|
|
151
|
+
'channel',
|
|
152
|
+
'thread',
|
|
153
|
+
'thread_comment',
|
|
154
|
+
'inbox_thread',
|
|
155
|
+
'inbox_thread_comment',
|
|
156
|
+
'saved_thread',
|
|
157
|
+
'saved_thread_comment',
|
|
158
|
+
'conversation',
|
|
159
|
+
'message',
|
|
160
|
+
];
|
|
161
|
+
/**
|
|
162
|
+
* Maps the link parser's type names onto the types this SDK exposes. A name
|
|
163
|
+
* that is missing here is one this SDK does not know about, and is treated as
|
|
164
|
+
* an unrecognised URL rather than surfaced as a value outside the union.
|
|
165
|
+
*/
|
|
166
|
+
const LINK_TYPE_BY_PARSED_NAME = {
|
|
167
|
+
Channel: 'channel',
|
|
168
|
+
Thread: 'thread',
|
|
169
|
+
ThreadComment: 'thread_comment',
|
|
170
|
+
InboxThread: 'inbox_thread',
|
|
171
|
+
InboxThreadComment: 'inbox_thread_comment',
|
|
172
|
+
SavedThread: 'saved_thread',
|
|
173
|
+
SavedThreadComment: 'saved_thread_comment',
|
|
174
|
+
Conversation: 'conversation',
|
|
175
|
+
Message: 'message',
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Parse a Comms URL into the entity it points at.
|
|
179
|
+
*
|
|
180
|
+
* @param url The URL to parse.
|
|
181
|
+
* @returns The entity the URL points at, or `null` if it is not a Comms URL.
|
|
182
|
+
*/
|
|
183
|
+
export function parseCommsURL(url) {
|
|
184
|
+
const link = parseCommsLink(url);
|
|
185
|
+
if (!link) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const type = LINK_TYPE_BY_PARSED_NAME[link.type.name];
|
|
189
|
+
if (!type) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
type,
|
|
194
|
+
workspaceId: link.workspaceId,
|
|
195
|
+
channelId: link.channelId ?? null,
|
|
196
|
+
threadId: link.threadId ?? null,
|
|
197
|
+
commentId: link.commentId ?? null,
|
|
198
|
+
conversationId: link.conversationId ?? null,
|
|
199
|
+
messageId: link.messageId ?? null,
|
|
200
|
+
isQuoteReference: link.isQuoteReference,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -2,5 +2,5 @@ import { CustomError } from 'ts-custom-error';
|
|
|
2
2
|
export declare class CommsRequestError extends CustomError {
|
|
3
3
|
httpStatusCode?: number;
|
|
4
4
|
responseData?: unknown;
|
|
5
|
-
constructor(message: string, httpStatusCode?: number, responseData?: unknown);
|
|
5
|
+
constructor(message: string, httpStatusCode?: number, responseData?: unknown, options?: ErrorOptions);
|
|
6
6
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Helper functions for creating Comms permalinks (`https://comms.todoist.com/...`).
|
|
3
|
+
*
|
|
4
|
+
* Paths come from `@doist/sdk-kmp`, the shared implementation of Comms link
|
|
5
|
+
* routes, so they stay in step with the other Comms clients. The path builders
|
|
6
|
+
* are used rather than the URL builders so that `getFullCommsURL` can keep
|
|
7
|
+
* serving an arbitrary base URL.
|
|
3
8
|
*/
|
|
4
9
|
export type CommsURLParams = {
|
|
5
10
|
workspaceId: number;
|
|
@@ -72,6 +77,12 @@ export declare function getSavedThreadURL(params: {
|
|
|
72
77
|
workspaceId: number;
|
|
73
78
|
threadId: string;
|
|
74
79
|
}): string;
|
|
80
|
+
/** Returns the URL for a comment on a saved thread. */
|
|
81
|
+
export declare function getSavedThreadCommentURL(params: {
|
|
82
|
+
workspaceId: number;
|
|
83
|
+
threadId: string;
|
|
84
|
+
commentId: string;
|
|
85
|
+
}): string;
|
|
75
86
|
/** Returns the URL for the search root. */
|
|
76
87
|
export declare function getSearchRootURL(workspaceId: number): string;
|
|
77
88
|
/** Returns the URL for a search with a query. */
|
|
@@ -86,3 +97,33 @@ export declare function getSettingsURL(params: {
|
|
|
86
97
|
}): string;
|
|
87
98
|
/** Returns the URL for the team members root. */
|
|
88
99
|
export declare function getTeamMembersRootURL(workspaceId: number): string;
|
|
100
|
+
/** The kinds of Comms entity a URL can point at. */
|
|
101
|
+
export declare const COMMS_LINK_TYPES: readonly ["channel", "thread", "thread_comment", "inbox_thread", "inbox_thread_comment", "saved_thread", "saved_thread_comment", "conversation", "message"];
|
|
102
|
+
/** The kind of Comms entity a URL points at. */
|
|
103
|
+
export type CommsLinkType = (typeof COMMS_LINK_TYPES)[number];
|
|
104
|
+
/** The entity a Comms URL points at. */
|
|
105
|
+
export type CommsURLInfo = {
|
|
106
|
+
/** The kind of entity the URL points at. */
|
|
107
|
+
type: CommsLinkType;
|
|
108
|
+
/** The ID of the workspace the entity belongs to. */
|
|
109
|
+
workspaceId: string;
|
|
110
|
+
/** The ID of the channel, when the URL names one. */
|
|
111
|
+
channelId: string | null;
|
|
112
|
+
/** The ID of the thread, when the URL names one. */
|
|
113
|
+
threadId: string | null;
|
|
114
|
+
/** The ID of the comment, when the URL names one. */
|
|
115
|
+
commentId: string | null;
|
|
116
|
+
/** The ID of the conversation, when the URL names one. */
|
|
117
|
+
conversationId: string | null;
|
|
118
|
+
/** The ID of the message, when the URL names one. */
|
|
119
|
+
messageId: string | null;
|
|
120
|
+
/** Whether the URL points at a quoted reference rather than the entity itself. */
|
|
121
|
+
isQuoteReference: boolean;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Parse a Comms URL into the entity it points at.
|
|
125
|
+
*
|
|
126
|
+
* @param url The URL to parse.
|
|
127
|
+
* @returns The entity the URL points at, or `null` if it is not a Comms URL.
|
|
128
|
+
*/
|
|
129
|
+
export declare function parseCommsURL(url: string): CommsURLInfo | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doist/comms-sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "A TypeScript wrapper for the Comms REST API.",
|
|
5
5
|
"author": "Doist developers",
|
|
6
6
|
"homepage": "https://doist.github.io/comms-sdk-typescript/",
|
|
@@ -15,14 +15,11 @@
|
|
|
15
15
|
"node": ">=24",
|
|
16
16
|
"npm": ">=11"
|
|
17
17
|
},
|
|
18
|
-
"main": "dist/cjs/index.js",
|
|
19
|
-
"module": "dist/esm/index.js",
|
|
20
18
|
"types": "dist/types/index.d.ts",
|
|
21
19
|
"exports": {
|
|
22
20
|
".": {
|
|
23
21
|
"types": "./dist/types/index.d.ts",
|
|
24
|
-
"
|
|
25
|
-
"require": "./dist/cjs/index.js"
|
|
22
|
+
"default": "./dist/esm/index.js"
|
|
26
23
|
}
|
|
27
24
|
},
|
|
28
25
|
"sideEffects": false,
|
|
@@ -31,22 +28,21 @@
|
|
|
31
28
|
"test": "vitest run",
|
|
32
29
|
"test:watch": "vitest",
|
|
33
30
|
"test:coverage": "vitest run --coverage",
|
|
34
|
-
"build:cjs": "npx tsc -p tsconfig.cjs.json",
|
|
35
31
|
"build:esm": "npx tsc -p tsconfig.esm.json",
|
|
36
32
|
"build:fix-esm": "node scripts/fix-esm-imports.cjs",
|
|
37
33
|
"build:fix-dts": "node scripts/fix-dts-imports.cjs",
|
|
38
|
-
"build
|
|
39
|
-
"build": "npm-run-all clean build:cjs build:esm build:fix-esm build:fix-dts build:post",
|
|
34
|
+
"build": "npm-run-all clean build:esm build:fix-esm build:fix-dts",
|
|
40
35
|
"type-check": "npx tsc --noEmit",
|
|
41
36
|
"check": "oxlint src && oxfmt --check",
|
|
42
37
|
"check:fix": "oxlint src --fix && oxfmt",
|
|
43
|
-
"attw": "attw --pack --ignore-rules
|
|
38
|
+
"attw": "attw --pack --ignore-rules cjs-resolves-to-esm",
|
|
44
39
|
"audit": "npm audit --audit-level=moderate",
|
|
45
40
|
"integrity-checks": "npm-run-all clean check test build attw",
|
|
46
41
|
"prepublishOnly": "npm run integrity-checks",
|
|
47
42
|
"prepare": "npm run build"
|
|
48
43
|
},
|
|
49
44
|
"dependencies": {
|
|
45
|
+
"@doist/sdk-kmp": "0.2.3",
|
|
50
46
|
"camelcase": "9.0.0",
|
|
51
47
|
"ts-custom-error": "3.3.1",
|
|
52
48
|
"undici": "7.29.0",
|
|
@@ -81,7 +77,6 @@
|
|
|
81
77
|
"*.{ts,tsx,json,md,yml,yaml}": "oxfmt --no-error-on-unmatched-pattern"
|
|
82
78
|
},
|
|
83
79
|
"files": [
|
|
84
|
-
"dist/cjs/**/*",
|
|
85
80
|
"dist/esm/**/*",
|
|
86
81
|
"dist/types/**/*",
|
|
87
82
|
"!dist/**/*.test.js",
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TOKEN_ENDPOINT_AUTH_METHODS = exports.COMMS_SCOPES = void 0;
|
|
4
|
-
exports.getAuthStateParameter = getAuthStateParameter;
|
|
5
|
-
exports.getAuthorizationUrl = getAuthorizationUrl;
|
|
6
|
-
exports.getAuthToken = getAuthToken;
|
|
7
|
-
exports.refreshAuthToken = refreshAuthToken;
|
|
8
|
-
exports.revokeAuthToken = revokeAuthToken;
|
|
9
|
-
exports.registerClient = registerClient;
|
|
10
|
-
const uuid_1 = require("uuid");
|
|
11
|
-
const http_client_1 = require("./transport/http-client");
|
|
12
|
-
const errors_1 = require("./types/errors");
|
|
13
|
-
/**
|
|
14
|
-
* Default origin of the Comms authorization server.
|
|
15
|
-
*
|
|
16
|
-
* The authorization server for Comms is Todoist (`todoist.com`), not the
|
|
17
|
-
* resource server (`comms.todoist.com`). Tokens are issued by Todoist and
|
|
18
|
-
* accepted by the Comms API. Paths match Todoist's published
|
|
19
|
-
* `/.well-known/oauth-authorization-server` metadata.
|
|
20
|
-
*/
|
|
21
|
-
const DEFAULT_AUTH_BASE_URL = 'https://todoist.com';
|
|
22
|
-
function getAuthBaseUrl(baseUrl) {
|
|
23
|
-
const base = baseUrl ?? DEFAULT_AUTH_BASE_URL;
|
|
24
|
-
return base.endsWith('/') ? base.slice(0, -1) : base;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* OAuth scopes for the Comms API.
|
|
28
|
-
*
|
|
29
|
-
* @remarks
|
|
30
|
-
* Request only the scopes your application needs:
|
|
31
|
-
*
|
|
32
|
-
* **User Scopes:**
|
|
33
|
-
* - `user:read` - Access user's personal settings
|
|
34
|
-
* - `user:write` - Access and update user's personal settings
|
|
35
|
-
*
|
|
36
|
-
* **Workspace Scopes:**
|
|
37
|
-
* - `workspaces:read` - Access teams the user is part of
|
|
38
|
-
* - `workspaces:write` - Access and update teams the user is part of
|
|
39
|
-
*
|
|
40
|
-
* **Channel Scopes:**
|
|
41
|
-
* - `channels:read` - Access channels
|
|
42
|
-
* - `channels:write` - Access and update channels
|
|
43
|
-
* - `channels:remove` - Access, update, and delete channels
|
|
44
|
-
*
|
|
45
|
-
* **Thread Scopes:**
|
|
46
|
-
* - `threads:read` - Access threads
|
|
47
|
-
* - `threads:write` - Access and update threads
|
|
48
|
-
* - `threads:remove` - Access, update, and delete threads
|
|
49
|
-
*
|
|
50
|
-
* **Comment Scopes:**
|
|
51
|
-
* - `comments:read` - Access comments
|
|
52
|
-
* - `comments:write` - Access and update comments
|
|
53
|
-
* - `comments:remove` - Access, update, and delete comments
|
|
54
|
-
*
|
|
55
|
-
* **Group Scopes:**
|
|
56
|
-
* - `groups:read` - Access groups
|
|
57
|
-
* - `groups:write` - Access and update groups
|
|
58
|
-
* - `groups:remove` - Access, update, and delete groups
|
|
59
|
-
*
|
|
60
|
-
* **Message Scopes:**
|
|
61
|
-
* - `messages:read` - Access messages
|
|
62
|
-
* - `messages:write` - Access and update messages
|
|
63
|
-
* - `messages:remove` - Access, update, and delete messages
|
|
64
|
-
*
|
|
65
|
-
* **Reaction Scopes:**
|
|
66
|
-
* - `reactions:read` - Access reactions
|
|
67
|
-
* - `reactions:write` - Access and update reactions
|
|
68
|
-
* - `reactions:remove` - Access, update, and delete reactions
|
|
69
|
-
*
|
|
70
|
-
* **Search Scopes:**
|
|
71
|
-
* - `search:read` - Search
|
|
72
|
-
*
|
|
73
|
-
* **Attachment Scopes:**
|
|
74
|
-
* - `attachments:read` - Access attachments
|
|
75
|
-
* - `attachments:write` - Access and update attachments
|
|
76
|
-
*
|
|
77
|
-
* **Notification Scopes:**
|
|
78
|
-
* - `notifications:read` - Read user's notifications settings
|
|
79
|
-
* - `notifications:write` - Read and update user's notifications settings
|
|
80
|
-
*/
|
|
81
|
-
exports.COMMS_SCOPES = [
|
|
82
|
-
'user:read',
|
|
83
|
-
'user:write',
|
|
84
|
-
'workspaces:read',
|
|
85
|
-
'workspaces:write',
|
|
86
|
-
'channels:read',
|
|
87
|
-
'channels:write',
|
|
88
|
-
'channels:remove',
|
|
89
|
-
'threads:read',
|
|
90
|
-
'threads:write',
|
|
91
|
-
'threads:remove',
|
|
92
|
-
'comments:read',
|
|
93
|
-
'comments:write',
|
|
94
|
-
'comments:remove',
|
|
95
|
-
'groups:read',
|
|
96
|
-
'groups:write',
|
|
97
|
-
'groups:remove',
|
|
98
|
-
'messages:read',
|
|
99
|
-
'messages:write',
|
|
100
|
-
'messages:remove',
|
|
101
|
-
'reactions:read',
|
|
102
|
-
'reactions:write',
|
|
103
|
-
'reactions:remove',
|
|
104
|
-
'search:read',
|
|
105
|
-
'attachments:read',
|
|
106
|
-
'attachments:write',
|
|
107
|
-
'notifications:read',
|
|
108
|
-
'notifications:write',
|
|
109
|
-
];
|
|
110
|
-
/** Supported token endpoint authentication methods for dynamic client registration. */
|
|
111
|
-
exports.TOKEN_ENDPOINT_AUTH_METHODS = [
|
|
112
|
-
'client_secret_post',
|
|
113
|
-
'client_secret_basic',
|
|
114
|
-
'none',
|
|
115
|
-
];
|
|
116
|
-
function getAuthStateParameter() {
|
|
117
|
-
return (0, uuid_1.v4)();
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Generates the authorization URL for the OAuth2 flow.
|
|
121
|
-
*
|
|
122
|
-
* The `clientId` can be either a traditional client ID string (e.g. from
|
|
123
|
-
* {@link registerClient}) or an HTTPS URL pointing to a client metadata document,
|
|
124
|
-
* as defined in {@link https://drafts.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/ RFC draft-ietf-oauth-client-id-metadata-document}.
|
|
125
|
-
*/
|
|
126
|
-
function getAuthorizationUrl(clientId, scopes, state, redirectUri, baseUrl) {
|
|
127
|
-
if (!scopes?.length) {
|
|
128
|
-
throw new Error('At least one scope value is required.');
|
|
129
|
-
}
|
|
130
|
-
const authBaseUrl = `${getAuthBaseUrl(baseUrl)}/oauth`;
|
|
131
|
-
const scope = scopes.join(' ');
|
|
132
|
-
const params = new URLSearchParams({
|
|
133
|
-
client_id: clientId,
|
|
134
|
-
response_type: 'code',
|
|
135
|
-
scope,
|
|
136
|
-
state,
|
|
137
|
-
});
|
|
138
|
-
if (redirectUri) {
|
|
139
|
-
params.append('redirect_uri', redirectUri);
|
|
140
|
-
}
|
|
141
|
-
return `${authBaseUrl}/authorize?${params.toString()}`;
|
|
142
|
-
}
|
|
143
|
-
async function getAuthToken(args, options) {
|
|
144
|
-
const tokenUrl = `${getAuthBaseUrl(options?.baseUrl)}/oauth/access_token`;
|
|
145
|
-
const payload = {
|
|
146
|
-
clientId: args.clientId,
|
|
147
|
-
clientSecret: args.clientSecret,
|
|
148
|
-
code: args.code,
|
|
149
|
-
grantType: 'authorization_code',
|
|
150
|
-
...(args.redirectUri && { redirectUri: args.redirectUri }),
|
|
151
|
-
};
|
|
152
|
-
const response = await (0, http_client_1.request)({
|
|
153
|
-
httpMethod: 'POST',
|
|
154
|
-
baseUri: tokenUrl,
|
|
155
|
-
relativePath: '',
|
|
156
|
-
payload,
|
|
157
|
-
customFetch: options?.customFetch,
|
|
158
|
-
});
|
|
159
|
-
if (!(0, http_client_1.isSuccess)(response) || !response.data?.accessToken) {
|
|
160
|
-
throw new errors_1.CommsRequestError('Authentication token exchange failed.', response.status, response.data);
|
|
161
|
-
}
|
|
162
|
-
return response.data;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Exchanges a refresh token for a new access token using the OAuth2
|
|
166
|
-
* `refresh_token` grant.
|
|
167
|
-
*
|
|
168
|
-
* @example
|
|
169
|
-
* ```typescript
|
|
170
|
-
* const { accessToken, refreshToken } = await refreshAuthToken({
|
|
171
|
-
* clientId: 'client-id',
|
|
172
|
-
* clientSecret: 'client-secret',
|
|
173
|
-
* refreshToken: storedRefreshToken,
|
|
174
|
-
* })
|
|
175
|
-
* ```
|
|
176
|
-
*
|
|
177
|
-
* @returns The refreshed token. The response may include a new `refreshToken`
|
|
178
|
-
* (rotated by the server); persist it in place of the previous one when present.
|
|
179
|
-
* @throws {@link CommsRequestError} If the refresh fails
|
|
180
|
-
*/
|
|
181
|
-
async function refreshAuthToken(args, options) {
|
|
182
|
-
const tokenUrl = `${getAuthBaseUrl(options?.baseUrl)}/oauth/access_token`;
|
|
183
|
-
const payload = {
|
|
184
|
-
clientId: args.clientId,
|
|
185
|
-
clientSecret: args.clientSecret,
|
|
186
|
-
refreshToken: args.refreshToken,
|
|
187
|
-
grantType: 'refresh_token',
|
|
188
|
-
};
|
|
189
|
-
const response = await (0, http_client_1.request)({
|
|
190
|
-
httpMethod: 'POST',
|
|
191
|
-
baseUri: tokenUrl,
|
|
192
|
-
relativePath: '',
|
|
193
|
-
payload,
|
|
194
|
-
customFetch: options?.customFetch,
|
|
195
|
-
});
|
|
196
|
-
if (!(0, http_client_1.isSuccess)(response) || !response.data?.accessToken) {
|
|
197
|
-
throw new errors_1.CommsRequestError('Authentication token refresh failed.', response.status, response.data);
|
|
198
|
-
}
|
|
199
|
-
return response.data;
|
|
200
|
-
}
|
|
201
|
-
async function revokeAuthToken(args, options) {
|
|
202
|
-
const revokeUrl = `${getAuthBaseUrl(options?.baseUrl)}/api/v1/revoke`;
|
|
203
|
-
const response = await (0, http_client_1.request)({
|
|
204
|
-
httpMethod: 'POST',
|
|
205
|
-
baseUri: revokeUrl,
|
|
206
|
-
relativePath: '',
|
|
207
|
-
payload: {
|
|
208
|
-
clientId: args.clientId,
|
|
209
|
-
clientSecret: args.clientSecret,
|
|
210
|
-
token: args.accessToken,
|
|
211
|
-
},
|
|
212
|
-
customFetch: options?.customFetch,
|
|
213
|
-
});
|
|
214
|
-
return (0, http_client_1.isSuccess)(response);
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Registers a new OAuth client via Dynamic Client Registration (RFC 7591).
|
|
218
|
-
*
|
|
219
|
-
* @example
|
|
220
|
-
* ```typescript
|
|
221
|
-
* const client = await registerClient({
|
|
222
|
-
* redirectUris: ['https://example.com/callback'],
|
|
223
|
-
* clientName: 'My App',
|
|
224
|
-
* scope: ['user:read', 'channels:read'],
|
|
225
|
-
* })
|
|
226
|
-
* // Use client.clientId and client.clientSecret for OAuth flows
|
|
227
|
-
* ```
|
|
228
|
-
*
|
|
229
|
-
* @returns The registered client details
|
|
230
|
-
* @throws {@link CommsRequestError} If the registration fails
|
|
231
|
-
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591 RFC 7591}
|
|
232
|
-
*/
|
|
233
|
-
async function registerClient(args, options) {
|
|
234
|
-
const registerUrl = `${getAuthBaseUrl(options?.baseUrl)}/oauth/register`;
|
|
235
|
-
const response = await (0, http_client_1.request)({
|
|
236
|
-
httpMethod: 'POST',
|
|
237
|
-
baseUri: registerUrl,
|
|
238
|
-
relativePath: '',
|
|
239
|
-
payload: { ...args, scope: args.scope?.join(' ') },
|
|
240
|
-
customFetch: options?.customFetch,
|
|
241
|
-
});
|
|
242
|
-
if (!(0, http_client_1.isSuccess)(response) || !response.data?.clientId) {
|
|
243
|
-
throw new errors_1.CommsRequestError('Dynamic client registration failed.', response.status, response.data);
|
|
244
|
-
}
|
|
245
|
-
const { clientIdIssuedAt, clientSecretExpiresAt, scope, ...rest } = response.data;
|
|
246
|
-
return {
|
|
247
|
-
...rest,
|
|
248
|
-
scope: scope ? scope.split(' ') : undefined,
|
|
249
|
-
clientIdIssuedAt: clientIdIssuedAt !== undefined ? new Date(clientIdIssuedAt * 1000) : undefined,
|
|
250
|
-
clientSecretExpiresAt: clientSecretExpiresAt === undefined
|
|
251
|
-
? undefined
|
|
252
|
-
: clientSecretExpiresAt === 0
|
|
253
|
-
? null
|
|
254
|
-
: new Date(clientSecretExpiresAt * 1000),
|
|
255
|
-
};
|
|
256
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.addCommentRequest = addCommentRequest;
|
|
4
|
-
const endpoints_1 = require("../consts/endpoints");
|
|
5
|
-
const http_client_1 = require("../transport/http-client");
|
|
6
|
-
const uuidv7_1 = require("../utils/uuidv7");
|
|
7
|
-
const notify_audience_1 = require("./notify-audience");
|
|
8
|
-
/**
|
|
9
|
-
* Internal helper that powers `comments.createComment`,
|
|
10
|
-
* `threads.closeThread`, and `threads.reopenThread`.
|
|
11
|
-
*
|
|
12
|
-
* Normalizes the `notifyAudience` flag into a sentinel `groups` entry,
|
|
13
|
-
* rejects sentinel IDs passed via `groups` / `directGroupMentions`, mints a
|
|
14
|
-
* UUIDv7 `id` when the caller omits one, and posts to `/comments/add`. When
|
|
15
|
-
* `threadAction` is set (`'close'` / `'reopen'`), it is forwarded on the
|
|
16
|
-
* wire so the same request both adds the comment and transitions the
|
|
17
|
-
* parent thread.
|
|
18
|
-
*
|
|
19
|
-
* @param context - Per-call client context (base URI, API token, optional `customFetch`).
|
|
20
|
-
* @param params - The comment payload (`{@link CreateCommentArgs}`).
|
|
21
|
-
* @param options - Optional configuration.
|
|
22
|
-
* @param options.threadAction - When set, also transitions the parent thread (`'close'` or `'reopen'`).
|
|
23
|
-
* @returns The parsed {@link Comment} returned by the API.
|
|
24
|
-
*/
|
|
25
|
-
function addCommentRequest(context, params, options) {
|
|
26
|
-
const normalized = (0, notify_audience_1.applyNotifyAudience)(params);
|
|
27
|
-
const withId = {
|
|
28
|
-
...normalized,
|
|
29
|
-
id: (0, uuidv7_1.resolveCreateId)(normalized.id),
|
|
30
|
-
threadId: (0, uuidv7_1.resolveReferenceId)(normalized.threadId, 'threadId'),
|
|
31
|
-
};
|
|
32
|
-
const payload = options?.threadAction
|
|
33
|
-
? { ...withId, threadAction: options.threadAction }
|
|
34
|
-
: withId;
|
|
35
|
-
return (0, http_client_1.request)({
|
|
36
|
-
httpMethod: 'POST',
|
|
37
|
-
baseUri: context.baseUri,
|
|
38
|
-
relativePath: `${endpoints_1.ENDPOINT_COMMENTS}/add`,
|
|
39
|
-
apiToken: context.apiToken,
|
|
40
|
-
payload,
|
|
41
|
-
customFetch: context.customFetch,
|
|
42
|
-
}).then((response) => context.schema.parse(response.data));
|
|
43
|
-
}
|