@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.
Files changed (49) hide show
  1. package/dist/esm/transport/fetch-with-retry.js +44 -2
  2. package/dist/esm/types/errors.js +2 -2
  3. package/dist/esm/utils/url-helpers.js +104 -16
  4. package/dist/types/types/errors.d.ts +1 -1
  5. package/dist/types/utils/url-helpers.d.ts +41 -0
  6. package/package.json +5 -10
  7. package/dist/cjs/authentication.js +0 -256
  8. package/dist/cjs/clients/add-comment-helper.js +0 -43
  9. package/dist/cjs/clients/attachments-client.js +0 -71
  10. package/dist/cjs/clients/base-client.js +0 -33
  11. package/dist/cjs/clients/channels-client.js +0 -211
  12. package/dist/cjs/clients/comments-client.js +0 -175
  13. package/dist/cjs/clients/conversation-messages-client.js +0 -169
  14. package/dist/cjs/clients/conversations-client.js +0 -283
  15. package/dist/cjs/clients/groups-client.js +0 -172
  16. package/dist/cjs/clients/hooks-client.js +0 -52
  17. package/dist/cjs/clients/inbox-client.js +0 -184
  18. package/dist/cjs/clients/notify-audience.js +0 -45
  19. package/dist/cjs/clients/reactions-client.js +0 -97
  20. package/dist/cjs/clients/search-client.js +0 -138
  21. package/dist/cjs/clients/threads-client.js +0 -364
  22. package/dist/cjs/clients/users-client.js +0 -326
  23. package/dist/cjs/clients/workspace-users-client.js +0 -252
  24. package/dist/cjs/clients/workspaces-client.js +0 -174
  25. package/dist/cjs/comms-api.js +0 -70
  26. package/dist/cjs/consts/endpoints.js +0 -34
  27. package/dist/cjs/index.js +0 -53
  28. package/dist/cjs/migration.js +0 -96
  29. package/dist/cjs/package.json +0 -1
  30. package/dist/cjs/testUtils/msw-handlers.js +0 -51
  31. package/dist/cjs/testUtils/msw-setup.js +0 -35
  32. package/dist/cjs/testUtils/obsidian-fetch-adapter.js +0 -53
  33. package/dist/cjs/testUtils/test-defaults.js +0 -106
  34. package/dist/cjs/transport/fetch-with-retry.js +0 -146
  35. package/dist/cjs/transport/http-client.js +0 -56
  36. package/dist/cjs/transport/http-dispatcher.js +0 -190
  37. package/dist/cjs/types/api-version.js +0 -8
  38. package/dist/cjs/types/entities.js +0 -496
  39. package/dist/cjs/types/enums.js +0 -37
  40. package/dist/cjs/types/errors.js +0 -12
  41. package/dist/cjs/types/http.js +0 -4
  42. package/dist/cjs/types/index.js +0 -22
  43. package/dist/cjs/types/requests.js +0 -183
  44. package/dist/cjs/utils/case-conversion.js +0 -54
  45. package/dist/cjs/utils/index.js +0 -19
  46. package/dist/cjs/utils/multipart-upload.js +0 -202
  47. package/dist/cjs/utils/timestamp-conversion.js +0 -58
  48. package/dist/cjs/utils/url-helpers.js +0 -133
  49. 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
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
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?.message ?? 'Request failed');
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
@@ -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
- let url = `/${workspaceId}/`;
42
+ const workspace = String(workspaceId);
16
43
  if (channelId) {
17
- url += `ch/${channelId}/`;
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
- url += `t/${threadId}/`;
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
- else if (threadId) {
26
- url += `inbox/t/${threadId}/`;
52
+ if (threadId) {
27
53
  if (commentId) {
28
- url += `c/${commentId}`;
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
- else if (conversationId) {
32
- url += `msg/${conversationId}/`;
58
+ if (conversationId) {
33
59
  if (messageId) {
34
- url += `m/${messageId}`;
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
- else if (userId) {
38
- url += `people/u/${userId}`;
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 url;
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
- return `/${params.workspaceId}/saved/t/${params.threadId}`;
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": "2.0.2",
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
- "import": "./dist/esm/index.js",
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:post": "echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
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 fallback-condition false-esm",
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
- }