@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import camelcase from 'camelcase';
|
|
2
|
+
function isObject(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function isArray(value) {
|
|
6
|
+
return Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function toSnakeCase(str) {
|
|
9
|
+
return str
|
|
10
|
+
.replace(/([A-Z])/g, '_$1')
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/^_/, '');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export function camelCaseKeys(obj) {
|
|
18
|
+
if (isArray(obj)) {
|
|
19
|
+
return obj.map(camelCaseKeys);
|
|
20
|
+
}
|
|
21
|
+
if (isObject(obj)) {
|
|
22
|
+
const result = {};
|
|
23
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
24
|
+
const camelKey = camelcase(key);
|
|
25
|
+
result[camelKey] = camelCaseKeys(value);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
return obj;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export function snakeCaseKeys(obj) {
|
|
35
|
+
if (isArray(obj)) {
|
|
36
|
+
return obj.map(snakeCaseKeys);
|
|
37
|
+
}
|
|
38
|
+
if (isObject(obj)) {
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
41
|
+
const snakeKey = toSnakeCase(key);
|
|
42
|
+
result[snakeKey] = snakeCaseKeys(value);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
return obj;
|
|
47
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a Unix timestamp (in seconds) to a Date object.
|
|
3
|
+
* @param timestamp - Unix timestamp in seconds
|
|
4
|
+
* @returns Date object
|
|
5
|
+
*/
|
|
6
|
+
export function timestampToDate(timestamp) {
|
|
7
|
+
return new Date(timestamp * 1000);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Recursively transforms all timestamp fields (ending in 'Ts') in an object to Date objects.
|
|
11
|
+
* Also renames the fields by removing the 'Ts' suffix.
|
|
12
|
+
* @param obj - The object to transform
|
|
13
|
+
* @returns The transformed object with Date fields
|
|
14
|
+
*/
|
|
15
|
+
export function transformTimestamps(obj) {
|
|
16
|
+
if (obj === null || obj === undefined) {
|
|
17
|
+
return obj;
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(obj)) {
|
|
20
|
+
return obj.map((item) => transformTimestamps(item));
|
|
21
|
+
}
|
|
22
|
+
if (typeof obj === 'object') {
|
|
23
|
+
const result = {};
|
|
24
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
25
|
+
// Check if the key ends with 'Ts' and the value is a number
|
|
26
|
+
if (key.endsWith('Ts') && typeof value === 'number') {
|
|
27
|
+
// Remove 'Ts' suffix and convert to Date
|
|
28
|
+
const newKey = key.slice(0, -2);
|
|
29
|
+
// If the base key already exists in the original object, use *Date suffix
|
|
30
|
+
// to avoid overwriting it (e.g. pinned + pinnedTs → pinned + pinnedDate)
|
|
31
|
+
const targetKey = newKey in obj ? `${newKey}Date` : newKey;
|
|
32
|
+
result[targetKey] = timestampToDate(value);
|
|
33
|
+
}
|
|
34
|
+
else if (typeof value === 'object' && value !== null) {
|
|
35
|
+
// Recursively transform nested objects
|
|
36
|
+
result[key] = transformTimestamps(value);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
result[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
return obj;
|
|
45
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for creating Comms permalinks (`https://comms.todoist.com/a/...`).
|
|
3
|
+
*/
|
|
4
|
+
const COMMS_BASE_URL = 'https://comms.todoist.com';
|
|
5
|
+
/**
|
|
6
|
+
* Builds a relative Comms URL based on the provided parameters
|
|
7
|
+
* @param params - URL parameters including workspace, channel, conversation, thread, etc.
|
|
8
|
+
* @returns A relative URL path
|
|
9
|
+
* @example
|
|
10
|
+
* getCommsURL({ workspaceId: 1, channelId: '7Yp...', threadId: '7Yq...' })
|
|
11
|
+
* // returns '/a/1/ch/7Yp.../t/7Yq.../'
|
|
12
|
+
*/
|
|
13
|
+
export function getCommsURL(params) {
|
|
14
|
+
const { workspaceId, channelId, conversationId, threadId, commentId, messageId, userId } = params;
|
|
15
|
+
let url = `/a/${workspaceId}/`;
|
|
16
|
+
if (channelId) {
|
|
17
|
+
url += `ch/${channelId}/`;
|
|
18
|
+
if (threadId) {
|
|
19
|
+
url += `t/${threadId}/`;
|
|
20
|
+
if (commentId) {
|
|
21
|
+
url += `c/${commentId}`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (threadId) {
|
|
26
|
+
url += `inbox/t/${threadId}/`;
|
|
27
|
+
if (commentId) {
|
|
28
|
+
url += `c/${commentId}`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (conversationId) {
|
|
32
|
+
url += `msg/${conversationId}/`;
|
|
33
|
+
if (messageId) {
|
|
34
|
+
url += `m/${messageId}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (userId) {
|
|
38
|
+
url += `people/u/${userId}`;
|
|
39
|
+
}
|
|
40
|
+
return url;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Builds a full Comms URL (with protocol and hostname) based on the provided parameters
|
|
44
|
+
* @param params - URL parameters including workspace, channel, conversation, thread, etc.
|
|
45
|
+
* @param baseUrl - Optional base URL (defaults to 'https://comms.todoist.com')
|
|
46
|
+
*/
|
|
47
|
+
export function getFullCommsURL(params, baseUrl = COMMS_BASE_URL) {
|
|
48
|
+
return `${baseUrl}${getCommsURL(params)}`;
|
|
49
|
+
}
|
|
50
|
+
/** Returns the URL for a thread in a channel. */
|
|
51
|
+
export function getThreadURL(params) {
|
|
52
|
+
return getCommsURL(params);
|
|
53
|
+
}
|
|
54
|
+
/** Returns the URL for a channel. */
|
|
55
|
+
export function getChannelURL(params) {
|
|
56
|
+
return getCommsURL(params);
|
|
57
|
+
}
|
|
58
|
+
/** Returns the URL for a conversation. */
|
|
59
|
+
export function getConversationURL(params) {
|
|
60
|
+
return getCommsURL(params);
|
|
61
|
+
}
|
|
62
|
+
/** Returns the URL for a specific message in a conversation. */
|
|
63
|
+
export function getMessageURL(params) {
|
|
64
|
+
return getCommsURL(params);
|
|
65
|
+
}
|
|
66
|
+
/** Returns the URL for a comment in a thread. */
|
|
67
|
+
export function getCommentURL(params) {
|
|
68
|
+
return getCommsURL(params);
|
|
69
|
+
}
|
|
70
|
+
/** Returns the URL for the threads root (channels view). */
|
|
71
|
+
export function getThreadsRootURL(workspaceId) {
|
|
72
|
+
return `/a/${workspaceId}/ch`;
|
|
73
|
+
}
|
|
74
|
+
/** Returns the URL for the inbox. */
|
|
75
|
+
export function getInboxURL(workspaceId, tab) {
|
|
76
|
+
const tabParam = tab ? `/${tab}` : '';
|
|
77
|
+
return `/a/${workspaceId}/inbox${tabParam}`;
|
|
78
|
+
}
|
|
79
|
+
/** Returns the URL for the messages/conversations root. */
|
|
80
|
+
export function getMessagesRootURL(workspaceId) {
|
|
81
|
+
return `/a/${workspaceId}/msg`;
|
|
82
|
+
}
|
|
83
|
+
/** Returns the URL for a user profile. */
|
|
84
|
+
export function getUserProfileURL(params) {
|
|
85
|
+
return `/a/${params.workspaceId}/people/u/${params.userId}`;
|
|
86
|
+
}
|
|
87
|
+
/** Returns the URL for the saved threads view. */
|
|
88
|
+
export function getSavedThreadsRootURL(workspaceId) {
|
|
89
|
+
return `/a/${workspaceId}/saved`;
|
|
90
|
+
}
|
|
91
|
+
/** Returns the URL for a saved thread. */
|
|
92
|
+
export function getSavedThreadURL(params) {
|
|
93
|
+
return `/a/${params.workspaceId}/saved/t/${params.threadId}`;
|
|
94
|
+
}
|
|
95
|
+
/** Returns the URL for the search root. */
|
|
96
|
+
export function getSearchRootURL(workspaceId) {
|
|
97
|
+
return `/a/${workspaceId}/search`;
|
|
98
|
+
}
|
|
99
|
+
/** Returns the URL for a search with a query. */
|
|
100
|
+
export function getSearchQueryURL(params) {
|
|
101
|
+
return `/a/${params.workspaceId}/search?q=${decodeURIComponent(params.query)}`;
|
|
102
|
+
}
|
|
103
|
+
/** Returns the URL for settings. */
|
|
104
|
+
export function getSettingsURL(params) {
|
|
105
|
+
return params.initialLocation
|
|
106
|
+
? `/a/${params.workspaceId}/settings/${params.initialLocation}`
|
|
107
|
+
: `/a/${params.workspaceId}/settings`;
|
|
108
|
+
}
|
|
109
|
+
/** Returns the URL for the team members root. */
|
|
110
|
+
export function getTeamMembersRootURL(workspaceId) {
|
|
111
|
+
return `/a/${workspaceId}/people/u`;
|
|
112
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { v7 as uuidv7 } from 'uuid';
|
|
2
|
+
/**
|
|
3
|
+
* ID utilities for entities that use opaque string identifiers (channels,
|
|
4
|
+
* threads, comments, conversations, messages, groups).
|
|
5
|
+
*
|
|
6
|
+
* Use {@link generateId} to mint a new ID locally and pass it to a creation
|
|
7
|
+
* endpoint. {@link encodeUuidToBase58} / {@link decodeBase58ToUuidBytes}
|
|
8
|
+
* expose the underlying encoding for callers that need to round-trip raw
|
|
9
|
+
* UUID bytes themselves.
|
|
10
|
+
*/
|
|
11
|
+
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
12
|
+
const BASE58_MAP = (() => {
|
|
13
|
+
const map = {};
|
|
14
|
+
for (let i = 0; i < BASE58_ALPHABET.length; i++) {
|
|
15
|
+
map[BASE58_ALPHABET[i]] = i;
|
|
16
|
+
}
|
|
17
|
+
return map;
|
|
18
|
+
})();
|
|
19
|
+
const UUID_BYTES_LEN = 16;
|
|
20
|
+
const UUID_BASE58_MAX_LEN = 22;
|
|
21
|
+
const HEX_RE = /^[0-9a-f]+$/;
|
|
22
|
+
export class UuidV7Error extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'UuidV7Error';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Encode a 16-byte UUID as a base58 string. */
|
|
29
|
+
export function encodeUuidToBase58(bytes) {
|
|
30
|
+
if (bytes.length !== UUID_BYTES_LEN) {
|
|
31
|
+
throw new UuidV7Error(`id must be ${UUID_BYTES_LEN} bytes`);
|
|
32
|
+
}
|
|
33
|
+
let zeros = 0;
|
|
34
|
+
while (zeros < bytes.length && bytes[zeros] === 0) {
|
|
35
|
+
zeros++;
|
|
36
|
+
}
|
|
37
|
+
let n = 0n;
|
|
38
|
+
for (const b of bytes) {
|
|
39
|
+
n = (n << 8n) | BigInt(b);
|
|
40
|
+
}
|
|
41
|
+
let out = '';
|
|
42
|
+
while (n > 0n) {
|
|
43
|
+
const rem = Number(n % 58n);
|
|
44
|
+
out = BASE58_ALPHABET[rem] + out;
|
|
45
|
+
n /= 58n;
|
|
46
|
+
}
|
|
47
|
+
return '1'.repeat(zeros) + out;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Decode a base58 string back into 16 UUID bytes. Throws {@link UuidV7Error}
|
|
51
|
+
* if the input is malformed or doesn't decode to exactly 16 bytes.
|
|
52
|
+
*/
|
|
53
|
+
export function decodeBase58ToUuidBytes(value) {
|
|
54
|
+
if (typeof value !== 'string') {
|
|
55
|
+
throw new UuidV7Error('id must be a base58 string');
|
|
56
|
+
}
|
|
57
|
+
if (value.length === 0) {
|
|
58
|
+
throw new UuidV7Error('id is empty');
|
|
59
|
+
}
|
|
60
|
+
if (value.length > UUID_BASE58_MAX_LEN) {
|
|
61
|
+
throw new UuidV7Error('id is too long');
|
|
62
|
+
}
|
|
63
|
+
let zeros = 0;
|
|
64
|
+
while (zeros < value.length && value[zeros] === '1') {
|
|
65
|
+
zeros++;
|
|
66
|
+
}
|
|
67
|
+
let n = 0n;
|
|
68
|
+
for (let i = 0; i < value.length; i++) {
|
|
69
|
+
const ch = value[i];
|
|
70
|
+
const v = BASE58_MAP[ch];
|
|
71
|
+
if (v === undefined) {
|
|
72
|
+
throw new UuidV7Error(`invalid base58 character: '${ch}'`);
|
|
73
|
+
}
|
|
74
|
+
n = n * 58n + BigInt(v);
|
|
75
|
+
}
|
|
76
|
+
const raw = [];
|
|
77
|
+
while (n > 0n) {
|
|
78
|
+
raw.unshift(Number(n & 0xffn));
|
|
79
|
+
n >>= 8n;
|
|
80
|
+
}
|
|
81
|
+
const padded = new Uint8Array(zeros + raw.length);
|
|
82
|
+
padded.set(raw, zeros);
|
|
83
|
+
if (padded.length !== UUID_BYTES_LEN) {
|
|
84
|
+
throw new UuidV7Error(`id must decode to ${UUID_BYTES_LEN} bytes`);
|
|
85
|
+
}
|
|
86
|
+
return padded;
|
|
87
|
+
}
|
|
88
|
+
function hexToBytes(hex) {
|
|
89
|
+
const out = new Uint8Array(hex.length / 2);
|
|
90
|
+
for (let i = 0; i < out.length; i++) {
|
|
91
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Mint a fresh ID. Callers should generate one of these locally when
|
|
97
|
+
* creating a new channel / thread / comment / conversation / message /
|
|
98
|
+
* group — the backend requires the client to supply the ID on create.
|
|
99
|
+
*/
|
|
100
|
+
export function generateId() {
|
|
101
|
+
const hex = uuidv7().replace(/-/g, '');
|
|
102
|
+
return encodeUuidToBase58(hexToBytes(hex));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolve the `id` for a create-style API call: validate the caller-supplied
|
|
106
|
+
* value (throwing {@link UuidV7Error} before the request leaves the SDK) or
|
|
107
|
+
* mint a fresh one via {@link generateId}.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveCreateId(id) {
|
|
110
|
+
if (id === undefined)
|
|
111
|
+
return generateId();
|
|
112
|
+
if (!isValidUuidV7Base58(id)) {
|
|
113
|
+
throw new UuidV7Error(`invalid id ${JSON.stringify(id)} — use generateId() or omit \`id\` and let the SDK mint one.`);
|
|
114
|
+
}
|
|
115
|
+
return id;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Validate that a value matches the expected ID format (the decoded bytes
|
|
119
|
+
* have the v7 version nibble + RFC 4122/9562 variant bits). Does NOT
|
|
120
|
+
* validate the embedded timestamp — the backend may still reject a value
|
|
121
|
+
* that is too far in the future or past.
|
|
122
|
+
*/
|
|
123
|
+
export function isValidUuidV7Base58(value) {
|
|
124
|
+
if (typeof value !== 'string')
|
|
125
|
+
return false;
|
|
126
|
+
let bytes;
|
|
127
|
+
try {
|
|
128
|
+
bytes = decodeBase58ToUuidBytes(value);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const versionNibble = bytes[6] & 0xf0;
|
|
134
|
+
const variantBits = bytes[8] & 0xc0;
|
|
135
|
+
return versionNibble === 0x70 && variantBits === 0x80;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Encode a canonical UUID string (hyphenated or not, any case) as a
|
|
139
|
+
* wire-format ID. Useful when interoperating with systems that hand you
|
|
140
|
+
* UUIDs in canonical form.
|
|
141
|
+
*/
|
|
142
|
+
export function base58FromUuidString(uuid) {
|
|
143
|
+
const stripped = uuid.replace(/-/g, '').toLowerCase();
|
|
144
|
+
if (stripped.length !== 32 || !HEX_RE.test(stripped)) {
|
|
145
|
+
throw new UuidV7Error('not a valid UUID string');
|
|
146
|
+
}
|
|
147
|
+
return encodeUuidToBase58(hexToBytes(stripped));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Inverse of {@link base58FromUuidString}: takes a wire-format ID and
|
|
151
|
+
* returns the canonical hyphenated UUID string.
|
|
152
|
+
*/
|
|
153
|
+
export function uuidStringFromBase58(value) {
|
|
154
|
+
const bytes = decodeBase58ToUuidBytes(value);
|
|
155
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
156
|
+
return [
|
|
157
|
+
hex.slice(0, 8),
|
|
158
|
+
hex.slice(8, 12),
|
|
159
|
+
hex.slice(12, 16),
|
|
160
|
+
hex.slice(16, 20),
|
|
161
|
+
hex.slice(20, 32),
|
|
162
|
+
].join('-');
|
|
163
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { CustomFetch } from './types/http.js';
|
|
2
|
+
export type AuthOptions = {
|
|
3
|
+
/** Optional custom base URL for OAuth endpoints */
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
/** Optional custom fetch implementation for cross-platform compatibility */
|
|
6
|
+
customFetch?: CustomFetch;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* OAuth scopes for the Comms API.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* Request only the scopes your application needs:
|
|
13
|
+
*
|
|
14
|
+
* **User Scopes:**
|
|
15
|
+
* - `user:read` - Access user's personal settings
|
|
16
|
+
* - `user:write` - Access and update user's personal settings
|
|
17
|
+
*
|
|
18
|
+
* **Workspace Scopes:**
|
|
19
|
+
* - `workspaces:read` - Access teams the user is part of
|
|
20
|
+
* - `workspaces:write` - Access and update teams the user is part of
|
|
21
|
+
*
|
|
22
|
+
* **Channel Scopes:**
|
|
23
|
+
* - `channels:read` - Access channels
|
|
24
|
+
* - `channels:write` - Access and update channels
|
|
25
|
+
* - `channels:remove` - Access, update, and delete channels
|
|
26
|
+
*
|
|
27
|
+
* **Thread Scopes:**
|
|
28
|
+
* - `threads:read` - Access threads
|
|
29
|
+
* - `threads:write` - Access and update threads
|
|
30
|
+
* - `threads:remove` - Access, update, and delete threads
|
|
31
|
+
*
|
|
32
|
+
* **Comment Scopes:**
|
|
33
|
+
* - `comments:read` - Access comments
|
|
34
|
+
* - `comments:write` - Access and update comments
|
|
35
|
+
* - `comments:remove` - Access, update, and delete comments
|
|
36
|
+
*
|
|
37
|
+
* **Group Scopes:**
|
|
38
|
+
* - `groups:read` - Access groups
|
|
39
|
+
* - `groups:write` - Access and update groups
|
|
40
|
+
* - `groups:remove` - Access, update, and delete groups
|
|
41
|
+
*
|
|
42
|
+
* **Message Scopes:**
|
|
43
|
+
* - `messages:read` - Access messages
|
|
44
|
+
* - `messages:write` - Access and update messages
|
|
45
|
+
* - `messages:remove` - Access, update, and delete messages
|
|
46
|
+
*
|
|
47
|
+
* **Reaction Scopes:**
|
|
48
|
+
* - `reactions:read` - Access reactions
|
|
49
|
+
* - `reactions:write` - Access and update reactions
|
|
50
|
+
* - `reactions:remove` - Access, update, and delete reactions
|
|
51
|
+
*
|
|
52
|
+
* **Search Scopes:**
|
|
53
|
+
* - `search:read` - Search
|
|
54
|
+
*
|
|
55
|
+
* **Attachment Scopes:**
|
|
56
|
+
* - `attachments:read` - Access attachments
|
|
57
|
+
* - `attachments:write` - Access and update attachments
|
|
58
|
+
*
|
|
59
|
+
* **Notification Scopes:**
|
|
60
|
+
* - `notifications:read` - Read user's notifications settings
|
|
61
|
+
* - `notifications:write` - Read and update user's notifications settings
|
|
62
|
+
*/
|
|
63
|
+
export declare const COMMS_SCOPES: readonly ["user:read", "user:write", "workspaces:read", "workspaces:write", "channels:read", "channels:write", "channels:remove", "threads:read", "threads:write", "threads:remove", "comments:read", "comments:write", "comments:remove", "groups:read", "groups:write", "groups:remove", "messages:read", "messages:write", "messages:remove", "reactions:read", "reactions:write", "reactions:remove", "search:read", "attachments:read", "attachments:write", "notifications:read", "notifications:write"];
|
|
64
|
+
/**
|
|
65
|
+
* Scopes determine what access a token has to the Comms API.
|
|
66
|
+
*/
|
|
67
|
+
export type CommsScope = (typeof COMMS_SCOPES)[number];
|
|
68
|
+
export type AuthTokenRequestArgs = {
|
|
69
|
+
clientId: string;
|
|
70
|
+
clientSecret: string;
|
|
71
|
+
code: string;
|
|
72
|
+
redirectUri?: string;
|
|
73
|
+
};
|
|
74
|
+
export type AuthTokenResponse = {
|
|
75
|
+
accessToken: string;
|
|
76
|
+
tokenType: string;
|
|
77
|
+
refreshToken?: string;
|
|
78
|
+
expiresIn?: number;
|
|
79
|
+
scope?: string;
|
|
80
|
+
};
|
|
81
|
+
export type RevokeAuthTokenRequestArgs = {
|
|
82
|
+
clientId: string;
|
|
83
|
+
clientSecret: string;
|
|
84
|
+
accessToken: string;
|
|
85
|
+
};
|
|
86
|
+
/** Supported token endpoint authentication methods for dynamic client registration. */
|
|
87
|
+
export declare const TOKEN_ENDPOINT_AUTH_METHODS: readonly ["client_secret_post", "client_secret_basic", "none"];
|
|
88
|
+
/**
|
|
89
|
+
* Authentication method used at the token endpoint.
|
|
90
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591#section-2 RFC 7591 Section 2}
|
|
91
|
+
*/
|
|
92
|
+
export type TokenEndpointAuthMethod = (typeof TOKEN_ENDPOINT_AUTH_METHODS)[number];
|
|
93
|
+
/**
|
|
94
|
+
* Parameters for registering a new OAuth client via Dynamic Client Registration.
|
|
95
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591 RFC 7591}
|
|
96
|
+
*/
|
|
97
|
+
export type ClientRegistrationRequest = {
|
|
98
|
+
redirectUris: string[];
|
|
99
|
+
clientName?: string;
|
|
100
|
+
clientUri?: string;
|
|
101
|
+
logoUri?: string;
|
|
102
|
+
scope?: readonly CommsScope[];
|
|
103
|
+
grantTypes?: string[];
|
|
104
|
+
responseTypes?: string[];
|
|
105
|
+
tokenEndpointAuthMethod?: TokenEndpointAuthMethod;
|
|
106
|
+
};
|
|
107
|
+
type RawClientRegistrationResponse = {
|
|
108
|
+
clientId: string;
|
|
109
|
+
clientSecret?: string;
|
|
110
|
+
clientName: string;
|
|
111
|
+
redirectUris: string[];
|
|
112
|
+
scope?: string;
|
|
113
|
+
grantTypes: string[];
|
|
114
|
+
responseTypes: string[];
|
|
115
|
+
tokenEndpointAuthMethod: string;
|
|
116
|
+
clientIdIssuedAt?: number;
|
|
117
|
+
clientSecretExpiresAt?: number;
|
|
118
|
+
clientUri?: string;
|
|
119
|
+
logoUri?: string;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Response from a successful dynamic client registration.
|
|
123
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.1 RFC 7591 Section 3.2.1}
|
|
124
|
+
*/
|
|
125
|
+
export type ClientRegistrationResponse = Omit<RawClientRegistrationResponse, 'clientIdIssuedAt' | 'clientSecretExpiresAt' | 'scope'> & {
|
|
126
|
+
scope?: CommsScope[];
|
|
127
|
+
clientIdIssuedAt?: Date;
|
|
128
|
+
/** `null` indicates the client secret never expires. Absent when no secret is issued. */
|
|
129
|
+
clientSecretExpiresAt?: Date | null;
|
|
130
|
+
};
|
|
131
|
+
export declare function getAuthStateParameter(): string;
|
|
132
|
+
/**
|
|
133
|
+
* Generates the authorization URL for the OAuth2 flow.
|
|
134
|
+
*
|
|
135
|
+
* The `clientId` can be either a traditional client ID string (e.g. from
|
|
136
|
+
* {@link registerClient}) or an HTTPS URL pointing to a client metadata document,
|
|
137
|
+
* as defined in {@link https://drafts.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/ RFC draft-ietf-oauth-client-id-metadata-document}.
|
|
138
|
+
*/
|
|
139
|
+
export declare function getAuthorizationUrl(clientId: string, scopes: CommsScope[], state: string, redirectUri?: string, baseUrl?: string): string;
|
|
140
|
+
export declare function getAuthToken(args: AuthTokenRequestArgs, options?: AuthOptions): Promise<AuthTokenResponse>;
|
|
141
|
+
export declare function revokeAuthToken(args: RevokeAuthTokenRequestArgs, options?: AuthOptions): Promise<boolean>;
|
|
142
|
+
/**
|
|
143
|
+
* Registers a new OAuth client via Dynamic Client Registration (RFC 7591).
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const client = await registerClient({
|
|
148
|
+
* redirectUris: ['https://example.com/callback'],
|
|
149
|
+
* clientName: 'My App',
|
|
150
|
+
* scope: ['user:read', 'channels:read'],
|
|
151
|
+
* })
|
|
152
|
+
* // Use client.clientId and client.clientSecret for OAuth flows
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* @returns The registered client details
|
|
156
|
+
* @throws {@link CommsRequestError} If the registration fails
|
|
157
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591 RFC 7591}
|
|
158
|
+
*/
|
|
159
|
+
export declare function registerClient(args: ClientRegistrationRequest, options?: AuthOptions): Promise<ClientRegistrationResponse>;
|
|
160
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Comment } from '../types/entities.js';
|
|
2
|
+
import type { CustomFetch } from '../types/http.js';
|
|
3
|
+
import type { CreateCommentArgs, ThreadAction } from '../types/requests.js';
|
|
4
|
+
type ClientContext = {
|
|
5
|
+
baseUri: string;
|
|
6
|
+
apiToken: string;
|
|
7
|
+
customFetch?: CustomFetch;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Internal helper that powers `comments.createComment`,
|
|
11
|
+
* `threads.closeThread`, and `threads.reopenThread`.
|
|
12
|
+
*
|
|
13
|
+
* Normalizes the `notifyAudience` flag into a sentinel `groups` entry,
|
|
14
|
+
* rejects sentinel IDs passed via `groups` / `directGroupMentions`, mints a
|
|
15
|
+
* UUIDv7 `id` when the caller omits one, and posts to `/comments/add`. When
|
|
16
|
+
* `threadAction` is set (`'close'` / `'reopen'`), it is forwarded on the
|
|
17
|
+
* wire so the same request both adds the comment and transitions the
|
|
18
|
+
* parent thread.
|
|
19
|
+
*
|
|
20
|
+
* @param context - Per-call client context (base URI, API token, optional `customFetch`).
|
|
21
|
+
* @param params - The comment payload (`{@link CreateCommentArgs}`).
|
|
22
|
+
* @param options - Optional configuration.
|
|
23
|
+
* @param options.threadAction - When set, also transitions the parent thread (`'close'` or `'reopen'`).
|
|
24
|
+
* @returns The parsed {@link Comment} returned by the API.
|
|
25
|
+
*/
|
|
26
|
+
export declare function addCommentRequest(context: ClientContext, params: CreateCommentArgs, options?: {
|
|
27
|
+
threadAction?: ThreadAction;
|
|
28
|
+
}): Promise<Comment>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ApiVersion } from '../types/api-version.js';
|
|
2
|
+
import type { CustomFetch } from '../types/http.js';
|
|
3
|
+
export type ClientConfig = {
|
|
4
|
+
/** API token for authentication */
|
|
5
|
+
apiToken: string;
|
|
6
|
+
/** Optional custom base URL. If not provided, uses the default Comms API URL */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
/** Optional API version. Defaults to 'v1' */
|
|
9
|
+
version?: ApiVersion;
|
|
10
|
+
/** Optional custom fetch implementation for cross-platform compatibility */
|
|
11
|
+
customFetch?: CustomFetch;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Base class for every Comms API client. Centralizes URL handling and
|
|
15
|
+
* config so individual clients stay focused on their endpoints.
|
|
16
|
+
*/
|
|
17
|
+
export declare class BaseClient {
|
|
18
|
+
protected readonly apiToken: string;
|
|
19
|
+
protected readonly baseUrl?: string;
|
|
20
|
+
protected readonly defaultVersion: ApiVersion;
|
|
21
|
+
protected readonly customFetch?: CustomFetch;
|
|
22
|
+
constructor(config: ClientConfig);
|
|
23
|
+
/**
|
|
24
|
+
* Returns the base URI for an API request, with a guaranteed trailing
|
|
25
|
+
* slash so relative paths resolve cleanly through `URL`.
|
|
26
|
+
*/
|
|
27
|
+
protected getBaseUri(): string;
|
|
28
|
+
}
|