@doist/comms-sdk 0.0.1 → 0.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/LICENSE +21 -0
- package/README.md +143 -45
- package/dist/cjs/authentication.js +211 -0
- package/dist/cjs/clients/add-comment-helper.js +70 -0
- package/dist/cjs/clients/base-client.js +25 -0
- package/dist/cjs/clients/channels-client.js +200 -0
- package/dist/cjs/clients/comments-client.js +159 -0
- package/dist/cjs/clients/conversation-messages-client.js +158 -0
- package/dist/cjs/clients/conversations-client.js +243 -0
- package/dist/cjs/clients/groups-client.js +164 -0
- package/dist/cjs/clients/inbox-client.js +171 -0
- package/dist/cjs/clients/reactions-client.js +97 -0
- package/dist/cjs/clients/search-client.js +138 -0
- package/dist/cjs/clients/threads-client.js +330 -0
- package/dist/cjs/clients/users-client.js +326 -0
- package/dist/cjs/clients/workspace-users-client.js +240 -0
- package/dist/cjs/clients/workspaces-client.js +166 -0
- package/dist/cjs/comms-api.js +66 -0
- package/dist/cjs/consts/endpoints.js +32 -0
- package/dist/cjs/index.js +48 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/testUtils/msw-handlers.js +51 -0
- package/dist/cjs/testUtils/msw-setup.js +21 -0
- package/dist/cjs/testUtils/obsidian-fetch-adapter.js +53 -0
- package/dist/cjs/testUtils/test-defaults.js +104 -0
- package/dist/cjs/transport/fetch-with-retry.js +136 -0
- package/dist/cjs/transport/http-client.js +56 -0
- package/dist/cjs/transport/http-dispatcher.js +143 -0
- package/dist/cjs/types/api-version.js +8 -0
- package/dist/cjs/types/entities.js +411 -0
- package/dist/cjs/types/enums.js +37 -0
- package/dist/cjs/types/errors.js +12 -0
- package/dist/cjs/types/http.js +4 -0
- package/dist/cjs/types/index.js +22 -0
- package/dist/cjs/types/requests.js +116 -0
- package/dist/cjs/utils/case-conversion.js +54 -0
- package/dist/cjs/utils/index.js +19 -0
- package/dist/cjs/utils/timestamp-conversion.js +49 -0
- package/dist/cjs/utils/url-helpers.js +131 -0
- package/dist/cjs/utils/uuidv7.js +174 -0
- package/dist/esm/authentication.js +203 -0
- package/dist/esm/clients/add-comment-helper.js +67 -0
- package/dist/esm/clients/base-client.js +21 -0
- package/dist/esm/clients/channels-client.js +196 -0
- package/dist/esm/clients/comments-client.js +155 -0
- package/dist/esm/clients/conversation-messages-client.js +154 -0
- package/dist/esm/clients/conversations-client.js +239 -0
- package/dist/esm/clients/groups-client.js +160 -0
- package/dist/esm/clients/inbox-client.js +167 -0
- package/dist/esm/clients/reactions-client.js +93 -0
- package/dist/esm/clients/search-client.js +134 -0
- package/dist/esm/clients/threads-client.js +326 -0
- package/dist/esm/clients/users-client.js +322 -0
- package/dist/esm/clients/workspace-users-client.js +236 -0
- package/dist/esm/clients/workspaces-client.js +162 -0
- package/dist/esm/comms-api.js +62 -0
- package/dist/esm/consts/endpoints.js +28 -0
- package/dist/esm/index.js +17 -0
- package/dist/esm/testUtils/msw-handlers.js +45 -0
- package/dist/esm/testUtils/msw-setup.js +18 -0
- package/dist/esm/testUtils/obsidian-fetch-adapter.js +50 -0
- package/dist/esm/testUtils/test-defaults.js +101 -0
- package/dist/esm/transport/fetch-with-retry.js +133 -0
- package/dist/esm/transport/http-client.js +51 -0
- package/dist/esm/transport/http-dispatcher.js +104 -0
- package/dist/esm/types/api-version.js +5 -0
- package/dist/esm/types/entities.js +408 -0
- package/dist/esm/types/enums.js +34 -0
- package/dist/esm/types/errors.js +8 -0
- package/dist/esm/types/http.js +1 -0
- package/dist/esm/types/index.js +6 -0
- package/dist/esm/types/requests.js +113 -0
- package/dist/esm/utils/case-conversion.js +47 -0
- package/dist/esm/utils/index.js +3 -0
- package/dist/esm/utils/timestamp-conversion.js +45 -0
- package/dist/esm/utils/url-helpers.js +112 -0
- package/dist/esm/utils/uuidv7.js +163 -0
- package/dist/types/authentication.d.ts +160 -0
- package/dist/types/clients/add-comment-helper.d.ts +29 -0
- package/dist/types/clients/base-client.d.ts +28 -0
- package/dist/types/clients/channels-client.d.ts +208 -0
- package/dist/types/clients/comments-client.d.ts +224 -0
- package/dist/types/clients/conversation-messages-client.d.ts +198 -0
- package/dist/types/clients/conversations-client.d.ts +346 -0
- package/dist/types/clients/groups-client.d.ts +148 -0
- package/dist/types/clients/inbox-client.d.ts +96 -0
- package/dist/types/clients/reactions-client.d.ts +57 -0
- package/dist/types/clients/search-client.d.ts +70 -0
- package/dist/types/clients/threads-client.d.ts +536 -0
- package/dist/types/clients/users-client.d.ts +250 -0
- package/dist/types/clients/workspace-users-client.d.ts +147 -0
- package/dist/types/clients/workspaces-client.d.ts +152 -0
- package/dist/types/comms-api.d.ts +62 -0
- package/dist/types/consts/endpoints.d.ts +24 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/testUtils/msw-handlers.d.ts +28 -0
- package/dist/types/testUtils/msw-setup.d.ts +1 -0
- package/dist/types/testUtils/obsidian-fetch-adapter.d.ts +29 -0
- package/dist/types/testUtils/test-defaults.d.ts +17 -0
- package/dist/types/transport/fetch-with-retry.d.ts +4 -0
- package/dist/types/transport/http-client.d.ts +13 -0
- package/dist/types/transport/http-dispatcher.d.ts +10 -0
- package/dist/types/types/api-version.d.ts +6 -0
- package/dist/types/types/entities.d.ts +1288 -0
- package/dist/types/types/enums.d.ts +55 -0
- package/dist/types/types/errors.d.ts +6 -0
- package/dist/types/types/http.d.ts +54 -0
- package/dist/types/types/index.d.ts +6 -0
- package/dist/types/types/requests.d.ts +366 -0
- package/dist/types/utils/case-conversion.d.ts +8 -0
- package/dist/types/utils/index.d.ts +3 -0
- package/dist/types/utils/timestamp-conversion.d.ts +13 -0
- package/dist/types/utils/url-helpers.d.ts +88 -0
- package/dist/types/utils/uuidv7.d.ts +40 -0
- package/package.json +91 -8
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Doist
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,45 +1,143 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
# Comms SDK TypeScript
|
|
2
|
+
|
|
3
|
+
The official TypeScript SDK for the Comms REST API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @doist/comms-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { CommsApi } from '@doist/comms-sdk'
|
|
15
|
+
|
|
16
|
+
const api = new CommsApi('YOUR_API_TOKEN')
|
|
17
|
+
|
|
18
|
+
api.users
|
|
19
|
+
.getSessionUser()
|
|
20
|
+
.then((user) => console.log(user))
|
|
21
|
+
.catch((error) => console.log(error))
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
By default the SDK targets production at `https://comms.todoist.com`.
|
|
25
|
+
Pass a `baseUrl` option to point at a different deployment — staging
|
|
26
|
+
lives at `https://comms.staging.todoist.com`:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
const api = new CommsApi('YOUR_API_TOKEN', {
|
|
30
|
+
baseUrl: 'https://comms.staging.todoist.com',
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Creating entities
|
|
35
|
+
|
|
36
|
+
Channel / thread / comment / conversation / message / group IDs are
|
|
37
|
+
opaque base58-encoded UUIDv7 strings; `workspaceId` and `userId` are
|
|
38
|
+
numeric.
|
|
39
|
+
|
|
40
|
+
Creation endpoints (`createChannel`, `createThread`, `createComment`,
|
|
41
|
+
`getOrCreateConversation`, `createMessage`, `createGroup`) accept an
|
|
42
|
+
optional `id`. **A caller-supplied `id` must be a base58-encoded
|
|
43
|
+
UUIDv7** — anything else fails fast with a `UuidV7Error` before the
|
|
44
|
+
request leaves the SDK. Either mint your own with `generateId()` (handy
|
|
45
|
+
for optimistic UI — the ID survives the round-trip unchanged) or omit
|
|
46
|
+
`id` and let the SDK mint one:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { CommsApi, generateId } from '@doist/comms-sdk'
|
|
50
|
+
|
|
51
|
+
const api = new CommsApi('YOUR_API_TOKEN')
|
|
52
|
+
|
|
53
|
+
// Option 1: let the SDK mint an ID
|
|
54
|
+
const channel = await api.channels.createChannel({
|
|
55
|
+
workspaceId: 1,
|
|
56
|
+
name: 'Engineering',
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Option 2: mint the ID yourself (must be a base58 UUIDv7 from generateId)
|
|
60
|
+
const id = generateId()
|
|
61
|
+
const sameChannel = await api.channels.createChannel({
|
|
62
|
+
workspaceId: 1,
|
|
63
|
+
name: 'Engineering',
|
|
64
|
+
id,
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Broadcast group markers
|
|
69
|
+
|
|
70
|
+
Use the string constants `EVERYONE` / `EVERYONE_IN_THREAD` when
|
|
71
|
+
populating `groups[]` / `directGroupMentions[]` directly, or pass
|
|
72
|
+
`notifyAudience` to `createComment` / `closeThread` / `reopenThread`
|
|
73
|
+
and let the SDK encode it for you:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
await api.comments.createComment({
|
|
77
|
+
threadId,
|
|
78
|
+
content: 'Heads up everyone',
|
|
79
|
+
notifyAudience: 'channel', // encoded as EVERYONE
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### OAuth 2.0
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { getAuthorizationUrl, getAuthToken, CommsApi } from '@doist/comms-sdk'
|
|
87
|
+
|
|
88
|
+
const authUrl = getAuthorizationUrl(
|
|
89
|
+
'your-client-id',
|
|
90
|
+
['user:read', 'channels:read'],
|
|
91
|
+
'state-parameter',
|
|
92
|
+
'https://yourapp.com/callback',
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const tokenResponse = await getAuthToken({
|
|
96
|
+
clientId: 'your-client-id',
|
|
97
|
+
clientSecret: 'your-client-secret',
|
|
98
|
+
code: 'authorization-code',
|
|
99
|
+
redirectUri: 'https://yourapp.com/callback',
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const api = new CommsApi(tokenResponse.accessToken)
|
|
103
|
+
const user = await api.users.getSessionUser()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Short-lived processes (CLIs, scripts)
|
|
107
|
+
|
|
108
|
+
On Node, the SDK keeps a connection pool alive across requests so HTTP/2
|
|
109
|
+
multiplexing and TLS reuse actually work. Long-running processes don't
|
|
110
|
+
need to think about it. **Short-lived processes should `await api.close()`
|
|
111
|
+
before exit**, otherwise Node's event loop waits ~4 seconds for idle
|
|
112
|
+
sockets to time out:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const api = new CommsApi('YOUR_API_TOKEN')
|
|
116
|
+
try {
|
|
117
|
+
await api.users.getSessionUser()
|
|
118
|
+
} finally {
|
|
119
|
+
await api.close()
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`api.close()` drains the process-global pool, so it also covers code
|
|
124
|
+
paths that only use the standalone OAuth helpers (`getAuthToken`,
|
|
125
|
+
`revokeAuthToken`, `registerClient`). Those flows can also import
|
|
126
|
+
`closeDefaultDispatcher` directly.
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
- `npm install`
|
|
131
|
+
- `npm test` — Vitest
|
|
132
|
+
- `npm run type-check` — TypeScript
|
|
133
|
+
- `npm run check` — oxlint + oxfmt
|
|
134
|
+
- `npm run build` — emit CJS + ESM + d.ts
|
|
135
|
+
|
|
136
|
+
## Releases
|
|
137
|
+
|
|
138
|
+
The package follows semantic versioning; releases publish to npm via the
|
|
139
|
+
GitHub workflow.
|
|
140
|
+
|
|
141
|
+
## Feedback
|
|
142
|
+
|
|
143
|
+
Open issues at https://github.com/Doist/comms-sdk-typescript.
|
|
@@ -0,0 +1,211 @@
|
|
|
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.revokeAuthToken = revokeAuthToken;
|
|
8
|
+
exports.registerClient = registerClient;
|
|
9
|
+
const uuid_1 = require("uuid");
|
|
10
|
+
const http_client_1 = require("./transport/http-client");
|
|
11
|
+
const errors_1 = require("./types/errors");
|
|
12
|
+
/**
|
|
13
|
+
* OAuth scopes for the Comms API.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* Request only the scopes your application needs:
|
|
17
|
+
*
|
|
18
|
+
* **User Scopes:**
|
|
19
|
+
* - `user:read` - Access user's personal settings
|
|
20
|
+
* - `user:write` - Access and update user's personal settings
|
|
21
|
+
*
|
|
22
|
+
* **Workspace Scopes:**
|
|
23
|
+
* - `workspaces:read` - Access teams the user is part of
|
|
24
|
+
* - `workspaces:write` - Access and update teams the user is part of
|
|
25
|
+
*
|
|
26
|
+
* **Channel Scopes:**
|
|
27
|
+
* - `channels:read` - Access channels
|
|
28
|
+
* - `channels:write` - Access and update channels
|
|
29
|
+
* - `channels:remove` - Access, update, and delete channels
|
|
30
|
+
*
|
|
31
|
+
* **Thread Scopes:**
|
|
32
|
+
* - `threads:read` - Access threads
|
|
33
|
+
* - `threads:write` - Access and update threads
|
|
34
|
+
* - `threads:remove` - Access, update, and delete threads
|
|
35
|
+
*
|
|
36
|
+
* **Comment Scopes:**
|
|
37
|
+
* - `comments:read` - Access comments
|
|
38
|
+
* - `comments:write` - Access and update comments
|
|
39
|
+
* - `comments:remove` - Access, update, and delete comments
|
|
40
|
+
*
|
|
41
|
+
* **Group Scopes:**
|
|
42
|
+
* - `groups:read` - Access groups
|
|
43
|
+
* - `groups:write` - Access and update groups
|
|
44
|
+
* - `groups:remove` - Access, update, and delete groups
|
|
45
|
+
*
|
|
46
|
+
* **Message Scopes:**
|
|
47
|
+
* - `messages:read` - Access messages
|
|
48
|
+
* - `messages:write` - Access and update messages
|
|
49
|
+
* - `messages:remove` - Access, update, and delete messages
|
|
50
|
+
*
|
|
51
|
+
* **Reaction Scopes:**
|
|
52
|
+
* - `reactions:read` - Access reactions
|
|
53
|
+
* - `reactions:write` - Access and update reactions
|
|
54
|
+
* - `reactions:remove` - Access, update, and delete reactions
|
|
55
|
+
*
|
|
56
|
+
* **Search Scopes:**
|
|
57
|
+
* - `search:read` - Search
|
|
58
|
+
*
|
|
59
|
+
* **Attachment Scopes:**
|
|
60
|
+
* - `attachments:read` - Access attachments
|
|
61
|
+
* - `attachments:write` - Access and update attachments
|
|
62
|
+
*
|
|
63
|
+
* **Notification Scopes:**
|
|
64
|
+
* - `notifications:read` - Read user's notifications settings
|
|
65
|
+
* - `notifications:write` - Read and update user's notifications settings
|
|
66
|
+
*/
|
|
67
|
+
exports.COMMS_SCOPES = [
|
|
68
|
+
'user:read',
|
|
69
|
+
'user:write',
|
|
70
|
+
'workspaces:read',
|
|
71
|
+
'workspaces:write',
|
|
72
|
+
'channels:read',
|
|
73
|
+
'channels:write',
|
|
74
|
+
'channels:remove',
|
|
75
|
+
'threads:read',
|
|
76
|
+
'threads:write',
|
|
77
|
+
'threads:remove',
|
|
78
|
+
'comments:read',
|
|
79
|
+
'comments:write',
|
|
80
|
+
'comments:remove',
|
|
81
|
+
'groups:read',
|
|
82
|
+
'groups:write',
|
|
83
|
+
'groups:remove',
|
|
84
|
+
'messages:read',
|
|
85
|
+
'messages:write',
|
|
86
|
+
'messages:remove',
|
|
87
|
+
'reactions:read',
|
|
88
|
+
'reactions:write',
|
|
89
|
+
'reactions:remove',
|
|
90
|
+
'search:read',
|
|
91
|
+
'attachments:read',
|
|
92
|
+
'attachments:write',
|
|
93
|
+
'notifications:read',
|
|
94
|
+
'notifications:write',
|
|
95
|
+
];
|
|
96
|
+
/** Supported token endpoint authentication methods for dynamic client registration. */
|
|
97
|
+
exports.TOKEN_ENDPOINT_AUTH_METHODS = [
|
|
98
|
+
'client_secret_post',
|
|
99
|
+
'client_secret_basic',
|
|
100
|
+
'none',
|
|
101
|
+
];
|
|
102
|
+
function getAuthStateParameter() {
|
|
103
|
+
return (0, uuid_1.v4)();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Generates the authorization URL for the OAuth2 flow.
|
|
107
|
+
*
|
|
108
|
+
* The `clientId` can be either a traditional client ID string (e.g. from
|
|
109
|
+
* {@link registerClient}) or an HTTPS URL pointing to a client metadata document,
|
|
110
|
+
* as defined in {@link https://drafts.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/ RFC draft-ietf-oauth-client-id-metadata-document}.
|
|
111
|
+
*/
|
|
112
|
+
function getAuthorizationUrl(clientId, scopes, state, redirectUri, baseUrl) {
|
|
113
|
+
if (!scopes?.length) {
|
|
114
|
+
throw new Error('At least one scope value is required.');
|
|
115
|
+
}
|
|
116
|
+
const authBaseUrl = baseUrl ? `${baseUrl}/oauth` : 'https://comms.todoist.com/oauth';
|
|
117
|
+
const scope = scopes.join(' ');
|
|
118
|
+
const params = new URLSearchParams({
|
|
119
|
+
client_id: clientId,
|
|
120
|
+
response_type: 'code',
|
|
121
|
+
scope,
|
|
122
|
+
state,
|
|
123
|
+
});
|
|
124
|
+
if (redirectUri) {
|
|
125
|
+
params.append('redirect_uri', redirectUri);
|
|
126
|
+
}
|
|
127
|
+
return `${authBaseUrl}/authorize?${params.toString()}`;
|
|
128
|
+
}
|
|
129
|
+
async function getAuthToken(args, options) {
|
|
130
|
+
const tokenUrl = options?.baseUrl
|
|
131
|
+
? `${options.baseUrl}/oauth/token`
|
|
132
|
+
: 'https://comms.todoist.com/oauth/token';
|
|
133
|
+
const payload = {
|
|
134
|
+
clientId: args.clientId,
|
|
135
|
+
clientSecret: args.clientSecret,
|
|
136
|
+
code: args.code,
|
|
137
|
+
grantType: 'authorization_code',
|
|
138
|
+
...(args.redirectUri && { redirectUri: args.redirectUri }),
|
|
139
|
+
};
|
|
140
|
+
const response = await (0, http_client_1.request)({
|
|
141
|
+
httpMethod: 'POST',
|
|
142
|
+
baseUri: tokenUrl,
|
|
143
|
+
relativePath: '',
|
|
144
|
+
payload,
|
|
145
|
+
customFetch: options?.customFetch,
|
|
146
|
+
});
|
|
147
|
+
if (!(0, http_client_1.isSuccess)(response) || !response.data?.accessToken) {
|
|
148
|
+
throw new errors_1.CommsRequestError('Authentication token exchange failed.', response.status, response.data);
|
|
149
|
+
}
|
|
150
|
+
return response.data;
|
|
151
|
+
}
|
|
152
|
+
async function revokeAuthToken(args, options) {
|
|
153
|
+
const revokeUrl = options?.baseUrl
|
|
154
|
+
? `${options.baseUrl}/oauth/revoke`
|
|
155
|
+
: 'https://comms.todoist.com/oauth/revoke';
|
|
156
|
+
const response = await (0, http_client_1.request)({
|
|
157
|
+
httpMethod: 'POST',
|
|
158
|
+
baseUri: revokeUrl,
|
|
159
|
+
relativePath: '',
|
|
160
|
+
payload: {
|
|
161
|
+
clientId: args.clientId,
|
|
162
|
+
clientSecret: args.clientSecret,
|
|
163
|
+
token: args.accessToken,
|
|
164
|
+
},
|
|
165
|
+
customFetch: options?.customFetch,
|
|
166
|
+
});
|
|
167
|
+
return (0, http_client_1.isSuccess)(response);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Registers a new OAuth client via Dynamic Client Registration (RFC 7591).
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* const client = await registerClient({
|
|
175
|
+
* redirectUris: ['https://example.com/callback'],
|
|
176
|
+
* clientName: 'My App',
|
|
177
|
+
* scope: ['user:read', 'channels:read'],
|
|
178
|
+
* })
|
|
179
|
+
* // Use client.clientId and client.clientSecret for OAuth flows
|
|
180
|
+
* ```
|
|
181
|
+
*
|
|
182
|
+
* @returns The registered client details
|
|
183
|
+
* @throws {@link CommsRequestError} If the registration fails
|
|
184
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591 RFC 7591}
|
|
185
|
+
*/
|
|
186
|
+
async function registerClient(args, options) {
|
|
187
|
+
const registerUrl = options?.baseUrl
|
|
188
|
+
? `${options.baseUrl}/oauth/register`
|
|
189
|
+
: 'https://comms.todoist.com/oauth/register';
|
|
190
|
+
const response = await (0, http_client_1.request)({
|
|
191
|
+
httpMethod: 'POST',
|
|
192
|
+
baseUri: registerUrl,
|
|
193
|
+
relativePath: '',
|
|
194
|
+
payload: { ...args, scope: args.scope?.join(' ') },
|
|
195
|
+
customFetch: options?.customFetch,
|
|
196
|
+
});
|
|
197
|
+
if (!(0, http_client_1.isSuccess)(response) || !response.data?.clientId) {
|
|
198
|
+
throw new errors_1.CommsRequestError('Dynamic client registration failed.', response.status, response.data);
|
|
199
|
+
}
|
|
200
|
+
const { clientIdIssuedAt, clientSecretExpiresAt, scope, ...rest } = response.data;
|
|
201
|
+
return {
|
|
202
|
+
...rest,
|
|
203
|
+
scope: scope ? scope.split(' ') : undefined,
|
|
204
|
+
clientIdIssuedAt: clientIdIssuedAt !== undefined ? new Date(clientIdIssuedAt * 1000) : undefined,
|
|
205
|
+
clientSecretExpiresAt: clientSecretExpiresAt === undefined
|
|
206
|
+
? undefined
|
|
207
|
+
: clientSecretExpiresAt === 0
|
|
208
|
+
? null
|
|
209
|
+
: new Date(clientSecretExpiresAt * 1000),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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 entities_1 = require("../types/entities");
|
|
7
|
+
const enums_1 = require("../types/enums");
|
|
8
|
+
const uuidv7_1 = require("../utils/uuidv7");
|
|
9
|
+
const SENTINEL_GROUP_IDS = new Set(Object.values(enums_1.NOTIFY_AUDIENCE_GROUP_IDS));
|
|
10
|
+
function isNotifyAudience(value) {
|
|
11
|
+
return typeof value === 'string' && enums_1.NOTIFY_AUDIENCES.includes(value);
|
|
12
|
+
}
|
|
13
|
+
function collectMarkerOffenses(field, values) {
|
|
14
|
+
if (!values)
|
|
15
|
+
return null;
|
|
16
|
+
const offending = values.filter((id) => SENTINEL_GROUP_IDS.has(id));
|
|
17
|
+
return offending.length > 0 ? { field, offending } : null;
|
|
18
|
+
}
|
|
19
|
+
function applyNotifyAudience(params) {
|
|
20
|
+
const offenses = [
|
|
21
|
+
collectMarkerOffenses('groups', params.groups ?? undefined),
|
|
22
|
+
collectMarkerOffenses('directGroupMentions', params.directGroupMentions ?? undefined),
|
|
23
|
+
].filter((o) => o !== null);
|
|
24
|
+
if (offenses.length > 0) {
|
|
25
|
+
const details = offenses
|
|
26
|
+
.map(({ field, offending }) => `\`${field}\` contains ${offending.join(', ')}`)
|
|
27
|
+
.join('; ');
|
|
28
|
+
throw new Error(`Reserved broadcast marker IDs found: ${details}. Pass these via \`notifyAudience\` on createComment / closeThread / reopenThread (e.g. \`notifyAudience: 'channel'\` for EVERYONE) instead of populating \`groups\` / \`directGroupMentions\` directly.`);
|
|
29
|
+
}
|
|
30
|
+
if (params.notifyAudience == null)
|
|
31
|
+
return params;
|
|
32
|
+
if (!isNotifyAudience(params.notifyAudience)) {
|
|
33
|
+
throw new Error(`Invalid \`notifyAudience\` value "${String(params.notifyAudience)}". Expected one of: ${enums_1.NOTIFY_AUDIENCES.join(', ')}.`);
|
|
34
|
+
}
|
|
35
|
+
const sentinel = enums_1.NOTIFY_AUDIENCE_GROUP_IDS[params.notifyAudience];
|
|
36
|
+
const { notifyAudience: _stripped, groups, ...rest } = params;
|
|
37
|
+
return { ...rest, groups: [...(groups ?? []), sentinel] };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Internal helper that powers `comments.createComment`,
|
|
41
|
+
* `threads.closeThread`, and `threads.reopenThread`.
|
|
42
|
+
*
|
|
43
|
+
* Normalizes the `notifyAudience` flag into a sentinel `groups` entry,
|
|
44
|
+
* rejects sentinel IDs passed via `groups` / `directGroupMentions`, mints a
|
|
45
|
+
* UUIDv7 `id` when the caller omits one, and posts to `/comments/add`. When
|
|
46
|
+
* `threadAction` is set (`'close'` / `'reopen'`), it is forwarded on the
|
|
47
|
+
* wire so the same request both adds the comment and transitions the
|
|
48
|
+
* parent thread.
|
|
49
|
+
*
|
|
50
|
+
* @param context - Per-call client context (base URI, API token, optional `customFetch`).
|
|
51
|
+
* @param params - The comment payload (`{@link CreateCommentArgs}`).
|
|
52
|
+
* @param options - Optional configuration.
|
|
53
|
+
* @param options.threadAction - When set, also transitions the parent thread (`'close'` or `'reopen'`).
|
|
54
|
+
* @returns The parsed {@link Comment} returned by the API.
|
|
55
|
+
*/
|
|
56
|
+
function addCommentRequest(context, params, options) {
|
|
57
|
+
const normalized = applyNotifyAudience(params);
|
|
58
|
+
const withId = { ...normalized, id: (0, uuidv7_1.resolveCreateId)(normalized.id) };
|
|
59
|
+
const payload = options?.threadAction
|
|
60
|
+
? { ...withId, threadAction: options.threadAction }
|
|
61
|
+
: withId;
|
|
62
|
+
return (0, http_client_1.request)({
|
|
63
|
+
httpMethod: 'POST',
|
|
64
|
+
baseUri: context.baseUri,
|
|
65
|
+
relativePath: `${endpoints_1.ENDPOINT_COMMENTS}/add`,
|
|
66
|
+
apiToken: context.apiToken,
|
|
67
|
+
payload,
|
|
68
|
+
customFetch: context.customFetch,
|
|
69
|
+
}).then((response) => entities_1.CommentSchema.parse(response.data));
|
|
70
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BaseClient = void 0;
|
|
4
|
+
const endpoints_1 = require("../consts/endpoints");
|
|
5
|
+
const api_version_1 = require("../types/api-version");
|
|
6
|
+
/**
|
|
7
|
+
* Base class for every Comms API client. Centralizes URL handling and
|
|
8
|
+
* config so individual clients stay focused on their endpoints.
|
|
9
|
+
*/
|
|
10
|
+
class BaseClient {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.apiToken = config.apiToken;
|
|
13
|
+
this.baseUrl = config.baseUrl;
|
|
14
|
+
this.defaultVersion = config.version || api_version_1.DEFAULT_API_VERSION;
|
|
15
|
+
this.customFetch = config.customFetch;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Returns the base URI for an API request, with a guaranteed trailing
|
|
19
|
+
* slash so relative paths resolve cleanly through `URL`.
|
|
20
|
+
*/
|
|
21
|
+
getBaseUri() {
|
|
22
|
+
return (0, endpoints_1.getCommsBaseUri)(this.defaultVersion, this.baseUrl);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.BaseClient = BaseClient;
|