@doist/comms-sdk 0.1.0-alpha.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 (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/dist/cjs/authentication.js +211 -0
  4. package/dist/cjs/clients/add-comment-helper.js +53 -0
  5. package/dist/cjs/clients/base-client.js +27 -0
  6. package/dist/cjs/clients/channels-client.js +83 -0
  7. package/dist/cjs/clients/comments-client.js +93 -0
  8. package/dist/cjs/clients/conversation-messages-client.js +87 -0
  9. package/dist/cjs/clients/conversations-client.js +103 -0
  10. package/dist/cjs/clients/groups-client.js +71 -0
  11. package/dist/cjs/clients/inbox-client.js +98 -0
  12. package/dist/cjs/clients/reactions-client.js +59 -0
  13. package/dist/cjs/clients/search-client.js +88 -0
  14. package/dist/cjs/clients/threads-client.js +135 -0
  15. package/dist/cjs/clients/users-client.js +199 -0
  16. package/dist/cjs/clients/workspace-users-client.js +140 -0
  17. package/dist/cjs/clients/workspaces-client.js +93 -0
  18. package/dist/cjs/comms-api.js +65 -0
  19. package/dist/cjs/consts/endpoints.js +27 -0
  20. package/dist/cjs/index.js +48 -0
  21. package/dist/cjs/package.json +1 -0
  22. package/dist/cjs/testUtils/msw-handlers.js +51 -0
  23. package/dist/cjs/testUtils/msw-setup.js +21 -0
  24. package/dist/cjs/testUtils/obsidian-fetch-adapter.js +53 -0
  25. package/dist/cjs/testUtils/test-defaults.js +102 -0
  26. package/dist/cjs/transport/fetch-with-retry.js +136 -0
  27. package/dist/cjs/transport/http-client.js +56 -0
  28. package/dist/cjs/transport/http-dispatcher.js +143 -0
  29. package/dist/cjs/types/entities.js +411 -0
  30. package/dist/cjs/types/enums.js +37 -0
  31. package/dist/cjs/types/errors.js +12 -0
  32. package/dist/cjs/types/http.js +4 -0
  33. package/dist/cjs/types/index.js +21 -0
  34. package/dist/cjs/types/requests.js +117 -0
  35. package/dist/cjs/utils/case-conversion.js +54 -0
  36. package/dist/cjs/utils/index.js +19 -0
  37. package/dist/cjs/utils/timestamp-conversion.js +49 -0
  38. package/dist/cjs/utils/url-helpers.js +131 -0
  39. package/dist/cjs/utils/uuidv7.js +174 -0
  40. package/dist/esm/authentication.js +203 -0
  41. package/dist/esm/clients/add-comment-helper.js +50 -0
  42. package/dist/esm/clients/base-client.js +23 -0
  43. package/dist/esm/clients/channels-client.js +79 -0
  44. package/dist/esm/clients/comments-client.js +89 -0
  45. package/dist/esm/clients/conversation-messages-client.js +83 -0
  46. package/dist/esm/clients/conversations-client.js +99 -0
  47. package/dist/esm/clients/groups-client.js +67 -0
  48. package/dist/esm/clients/inbox-client.js +94 -0
  49. package/dist/esm/clients/reactions-client.js +55 -0
  50. package/dist/esm/clients/search-client.js +84 -0
  51. package/dist/esm/clients/threads-client.js +131 -0
  52. package/dist/esm/clients/users-client.js +195 -0
  53. package/dist/esm/clients/workspace-users-client.js +136 -0
  54. package/dist/esm/clients/workspaces-client.js +89 -0
  55. package/dist/esm/comms-api.js +61 -0
  56. package/dist/esm/consts/endpoints.js +23 -0
  57. package/dist/esm/index.js +17 -0
  58. package/dist/esm/testUtils/msw-handlers.js +45 -0
  59. package/dist/esm/testUtils/msw-setup.js +18 -0
  60. package/dist/esm/testUtils/obsidian-fetch-adapter.js +50 -0
  61. package/dist/esm/testUtils/test-defaults.js +99 -0
  62. package/dist/esm/transport/fetch-with-retry.js +133 -0
  63. package/dist/esm/transport/http-client.js +51 -0
  64. package/dist/esm/transport/http-dispatcher.js +104 -0
  65. package/dist/esm/types/entities.js +408 -0
  66. package/dist/esm/types/enums.js +34 -0
  67. package/dist/esm/types/errors.js +8 -0
  68. package/dist/esm/types/http.js +1 -0
  69. package/dist/esm/types/index.js +5 -0
  70. package/dist/esm/types/requests.js +114 -0
  71. package/dist/esm/utils/case-conversion.js +47 -0
  72. package/dist/esm/utils/index.js +3 -0
  73. package/dist/esm/utils/timestamp-conversion.js +45 -0
  74. package/dist/esm/utils/url-helpers.js +112 -0
  75. package/dist/esm/utils/uuidv7.js +163 -0
  76. package/dist/types/authentication.d.ts +160 -0
  77. package/dist/types/clients/add-comment-helper.d.ts +12 -0
  78. package/dist/types/clients/base-client.d.ts +24 -0
  79. package/dist/types/clients/channels-client.d.ts +91 -0
  80. package/dist/types/clients/comments-client.d.ts +157 -0
  81. package/dist/types/clients/conversation-messages-client.d.ts +127 -0
  82. package/dist/types/clients/conversations-client.d.ts +206 -0
  83. package/dist/types/clients/groups-client.d.ts +55 -0
  84. package/dist/types/clients/inbox-client.d.ts +20 -0
  85. package/dist/types/clients/reactions-client.d.ts +19 -0
  86. package/dist/types/clients/search-client.d.ts +20 -0
  87. package/dist/types/clients/threads-client.d.ts +344 -0
  88. package/dist/types/clients/users-client.d.ts +123 -0
  89. package/dist/types/clients/workspace-users-client.d.ts +47 -0
  90. package/dist/types/clients/workspaces-client.d.ts +79 -0
  91. package/dist/types/comms-api.d.ts +59 -0
  92. package/dist/types/consts/endpoints.d.ts +19 -0
  93. package/dist/types/index.d.ts +18 -0
  94. package/dist/types/testUtils/msw-handlers.d.ts +28 -0
  95. package/dist/types/testUtils/msw-setup.d.ts +1 -0
  96. package/dist/types/testUtils/obsidian-fetch-adapter.d.ts +29 -0
  97. package/dist/types/testUtils/test-defaults.d.ts +16 -0
  98. package/dist/types/transport/fetch-with-retry.d.ts +4 -0
  99. package/dist/types/transport/http-client.d.ts +13 -0
  100. package/dist/types/transport/http-dispatcher.d.ts +10 -0
  101. package/dist/types/types/entities.d.ts +1288 -0
  102. package/dist/types/types/enums.d.ts +55 -0
  103. package/dist/types/types/errors.d.ts +6 -0
  104. package/dist/types/types/http.d.ts +54 -0
  105. package/dist/types/types/index.d.ts +5 -0
  106. package/dist/types/types/requests.d.ts +385 -0
  107. package/dist/types/utils/case-conversion.d.ts +8 -0
  108. package/dist/types/utils/index.d.ts +3 -0
  109. package/dist/types/utils/timestamp-conversion.d.ts +13 -0
  110. package/dist/types/utils/url-helpers.d.ts +88 -0
  111. package/dist/types/utils/uuidv7.d.ts +40 -0
  112. package/package.json +93 -0
@@ -0,0 +1,408 @@
1
+ import { z } from 'zod';
2
+ import { getFullCommsURL } from '../utils/url-helpers.js';
3
+ import { USER_TYPES } from './enums.js';
4
+ // EVERYONE / EVERYONE_IN_THREAD / GROUP_ID_MARKERS / GroupId / GroupIdMarker
5
+ // are defined in `./enums` (re-exported through `./index`) to keep this
6
+ // module from depending back on `./enums` for them. They're available
7
+ // from the same public surface (`@doist/comms-sdk`) either way.
8
+ // Reusable schema for system messages that can be either a string or an
9
+ // object. Nullable — the backend returns `null` when there is no system
10
+ // message.
11
+ export const SystemMessageSchema = z.union([z.string(), z.unknown()]).nullable().optional();
12
+ /**
13
+ * Shared `{ status: "ok" }` response shape. Pinned to the literal `'ok'`
14
+ * so a regression on the backend (e.g. a status code change) surfaces as
15
+ * a parse error here instead of being silently typed away. Most write
16
+ * endpoints that don't return an entity use this — archive / unarchive /
17
+ * mark-read / mark-all-read / mute / clear-unread / etc.
18
+ */
19
+ export const StatusOkSchema = z.object({ status: z.literal('ok') });
20
+ // Attachment entity from API. Mirrors the canonical backend shape produced
21
+ // by `unify_attachments` / `validate_file_attachment_json`. Only
22
+ // `attachmentId` and `urlType` are guaranteed; everything else depends on
23
+ // the attachment kind (file vs image vs link preview vs unfurled GIF).
24
+ // Loose: unknown keys from the backend pass through rather than being
25
+ // stripped, so newly-added or off-spec fields stay accessible to callers.
26
+ export const AttachmentSchema = z
27
+ .object({
28
+ attachmentId: z.string(),
29
+ urlType: z.string(),
30
+ title: z.string().nullable().optional(),
31
+ url: z.string().nullable().optional(),
32
+ fileName: z.string().nullable().optional(),
33
+ fileSize: z.number().int().nonnegative().nullable().optional(),
34
+ underlyingType: z.string().nullable().optional(),
35
+ description: z.string().nullable().optional(),
36
+ image: z.string().nullable().optional(),
37
+ imageWidth: z.number().int().nonnegative().nullable().optional(),
38
+ imageHeight: z.number().int().nonnegative().nullable().optional(),
39
+ duration: z.string().nullable().optional(),
40
+ uploadState: z.string().nullable().optional(),
41
+ video: z.string().nullable().optional(),
42
+ videoType: z.string().nullable().optional(),
43
+ videoAutoPlay: z.boolean().nullable().optional(),
44
+ })
45
+ .loose();
46
+ // Fields shared by `User` and `WorkspaceUser`. Profile fields
47
+ // (`fullName`, `imageId`, avatar URLs) are sourced from Todoist-ID.
48
+ export const BaseUserSchema = z.object({
49
+ id: z.number(),
50
+ fullName: z.string(),
51
+ shortName: z.string(),
52
+ firstName: z.string().nullable().optional(),
53
+ timezone: z.string(),
54
+ removed: z.boolean(),
55
+ imageId: z.string().nullable().optional(),
56
+ avatarUrls: z
57
+ .object({
58
+ s35: z.string(),
59
+ s60: z.string(),
60
+ s195: z.string(),
61
+ s640: z.string(),
62
+ })
63
+ .nullable()
64
+ .optional(),
65
+ restricted: z.boolean().nullable().optional(),
66
+ setupPending: z.boolean().nullable().optional(),
67
+ });
68
+ // User entity from API.
69
+ export const UserSchema = BaseUserSchema.extend({
70
+ email: z.email(),
71
+ clientId: z.string().nullable().optional(),
72
+ cometChannel: z.string().nullable().optional(),
73
+ lang: z.string(),
74
+ cometServer: z.string().nullable().optional(),
75
+ token: z.string().nullable().optional(),
76
+ scheduledBanners: z.array(z.string()).nullable().optional(),
77
+ });
78
+ // Workspace entity from API. `default_conversation` is a base58 UUIDv7
79
+ // string. `avatar_id` is kept (unlike the User rename to `image_id`,
80
+ // which is a User-only change per Comms_API_changes.md PR #125).
81
+ // `plan` is intentionally `z.string()` — see {@link WORKSPACE_PLANS}.
82
+ export const WorkspaceSchema = z.object({
83
+ id: z.number(),
84
+ name: z.string(),
85
+ defaultConversation: z.string().nullable().optional(),
86
+ creator: z.number(),
87
+ created: z.date(),
88
+ avatarId: z.string().nullable().optional(),
89
+ avatarUrls: z
90
+ .object({
91
+ s35: z.string(),
92
+ s60: z.string(),
93
+ s195: z.string(),
94
+ s640: z.string(),
95
+ })
96
+ .nullable()
97
+ .optional(),
98
+ plan: z.string().nullable().optional(),
99
+ });
100
+ export const ChannelSchema = z
101
+ .object({
102
+ id: z.string(),
103
+ name: z.string(),
104
+ description: z.string().nullable().optional(),
105
+ creator: z.number(),
106
+ userIds: z.array(z.number()).nullable().optional(),
107
+ color: z.number().nullable().optional(),
108
+ public: z.boolean(),
109
+ workspaceId: z.number(),
110
+ archived: z.boolean(),
111
+ created: z.date(),
112
+ useDefaultRecipients: z.boolean().nullable().optional(),
113
+ defaultGroups: z.array(z.string()).nullable().optional(),
114
+ defaultRecipients: z.array(z.number()).nullable().optional(),
115
+ isFavorited: z.boolean().nullable().optional(),
116
+ icon: z.number().nullable().optional(),
117
+ version: z.number(),
118
+ filters: z.record(z.string(), z.string()).nullable().optional(),
119
+ })
120
+ .transform((data) => ({
121
+ ...data,
122
+ url: getFullCommsURL({ workspaceId: data.workspaceId, channelId: data.id }),
123
+ }));
124
+ // Thread entity from API. `pinned` (boolean) and `pinnedTs` (epoch ms or
125
+ // null) are both surfaced — `pinned` is kept for Zapier/webhook clients.
126
+ export const ThreadSchema = z
127
+ .object({
128
+ id: z.string(),
129
+ title: z.string(),
130
+ content: z.string(),
131
+ creator: z.number(),
132
+ creatorName: z.string().nullable().optional(),
133
+ channelId: z.string(),
134
+ workspaceId: z.number(),
135
+ actions: z.array(z.unknown()).nullable().optional(),
136
+ attachments: z.array(AttachmentSchema).nullable().optional(),
137
+ commentCount: z.number(),
138
+ closed: z.boolean().nullable().optional(),
139
+ directGroupMentions: z.array(z.string()).nullable().optional(),
140
+ directMentions: z.array(z.number()).nullable().optional(),
141
+ groups: z.array(z.string()).nullable().optional(),
142
+ lastEdited: z.date().nullable().optional(),
143
+ lastObjIndex: z.number().nullable().optional(),
144
+ lastUpdated: z.date(),
145
+ mutedUntil: z.date().nullable().optional(),
146
+ participants: z.array(z.number()).nullable().optional(),
147
+ // Backend wire shape only includes `pinned_ts` (epoch ms or null);
148
+ // derive `pinned` from `pinnedTs != null` if you need a bool.
149
+ pinned: z.boolean().optional(),
150
+ pinnedTs: z.number().int().nullable().optional(),
151
+ posted: z.date(),
152
+ reactions: z.record(z.string(), z.unknown()).nullable().optional(),
153
+ recipients: z.array(z.number()).nullable().optional(),
154
+ responders: z.array(z.number()).nullable().optional(),
155
+ snippet: z.string(),
156
+ snippetCreator: z.number(),
157
+ snippetMaskAvatarUrl: z.string().nullable().optional(),
158
+ snippetMaskPoster: z.string().nullable().optional(),
159
+ systemMessage: SystemMessageSchema,
160
+ toEmails: z.array(z.string()).nullable().optional(),
161
+ isArchived: z.boolean(),
162
+ isSaved: z.boolean().nullable().optional(),
163
+ inInbox: z.boolean().nullable().optional(),
164
+ lastComment: z
165
+ .object({
166
+ id: z.string(),
167
+ content: z.string(),
168
+ creator: z.number(),
169
+ creatorName: z.string(),
170
+ threadId: z.string(),
171
+ channelId: z.string(),
172
+ posted: z.date(),
173
+ systemMessage: SystemMessageSchema,
174
+ attachments: z.array(AttachmentSchema).nullable().optional(),
175
+ reactions: z.record(z.string(), z.array(z.number())).nullable().optional(),
176
+ actions: z.array(z.unknown()).nullable().optional(),
177
+ objIndex: z.number(),
178
+ lastEdited: z.date().nullable().optional(),
179
+ deleted: z.boolean(),
180
+ deletedBy: z.number().nullable().optional(),
181
+ directGroupMentions: z.array(z.string()).nullable().optional(),
182
+ directMentions: z.array(z.number()).nullable().optional(),
183
+ groups: z.array(z.string()).nullable().optional(),
184
+ recipients: z.array(z.number()).nullable().optional(),
185
+ toEmails: z.array(z.string()).nullable().optional(),
186
+ version: z.number(),
187
+ workspaceId: z.number(),
188
+ })
189
+ .nullable()
190
+ .optional(),
191
+ })
192
+ .transform((data) => ({
193
+ ...data,
194
+ url: getFullCommsURL({
195
+ workspaceId: data.workspaceId,
196
+ channelId: data.channelId,
197
+ threadId: data.id,
198
+ }),
199
+ }));
200
+ // Group entity from API. The broadcast markers `EVERYONE` and
201
+ // `EVERYONE_IN_THREAD` can appear in place of a real `id` in group-bearing
202
+ // fields on threads/comments/channels.
203
+ export const GroupSchema = z.object({
204
+ id: z.string(),
205
+ name: z.string(),
206
+ description: z.string().nullable().optional(),
207
+ workspaceId: z.number(),
208
+ userIds: z.array(z.number()),
209
+ version: z.number(),
210
+ });
211
+ // Conversation entity from API.
212
+ export const ConversationSchema = z
213
+ .object({
214
+ id: z.string(),
215
+ workspaceId: z.number(),
216
+ userIds: z.array(z.number()),
217
+ messageCount: z.number().nullable().optional(),
218
+ lastObjIndex: z.number(),
219
+ snippet: z.string(),
220
+ snippetCreators: z.array(z.number()),
221
+ lastActive: z.date(),
222
+ mutedUntil: z.date().nullable().optional(),
223
+ archived: z.boolean(),
224
+ created: z.date(),
225
+ creator: z.number(),
226
+ title: z.string().nullable().optional(),
227
+ private: z.boolean().nullable().optional(),
228
+ lastMessage: z
229
+ .object({
230
+ // `id` may be string or number depending on the endpoint; coerced.
231
+ id: z.union([z.string(), z.number()]).transform(String),
232
+ content: z.string(),
233
+ creator: z.number(),
234
+ conversationId: z.string(),
235
+ posted: z.date(),
236
+ systemMessage: SystemMessageSchema,
237
+ attachments: z.array(AttachmentSchema).nullable().optional(),
238
+ reactions: z.record(z.string(), z.array(z.number())).nullable().optional(),
239
+ actions: z.array(z.unknown()).nullable().optional(),
240
+ objIndex: z.number().nullable().optional(),
241
+ lastEdited: z.date().nullable().optional(),
242
+ deleted: z.boolean().nullable().optional(),
243
+ directGroupMentions: z.array(z.string()).nullable().optional(),
244
+ directMentions: z.array(z.number()).nullable().optional(),
245
+ version: z.number().nullable().optional(),
246
+ workspaceId: z.number().nullable().optional(),
247
+ })
248
+ .nullable()
249
+ .optional(),
250
+ })
251
+ .transform((data) => ({
252
+ ...data,
253
+ url: getFullCommsURL({ workspaceId: data.workspaceId, conversationId: data.id }),
254
+ }));
255
+ export const CommentSchema = z
256
+ .object({
257
+ id: z.string(),
258
+ content: z.string(),
259
+ creator: z.number(),
260
+ threadId: z.string(),
261
+ workspaceId: z.number(),
262
+ conversationId: z.string().nullable().optional(),
263
+ posted: z.date(),
264
+ lastEdited: z.date().nullable().optional(),
265
+ directMentions: z.array(z.number()).nullable().optional(),
266
+ directGroupMentions: z.array(z.string()).nullable().optional(),
267
+ systemMessage: SystemMessageSchema,
268
+ attachments: z.array(AttachmentSchema).nullable().optional(),
269
+ reactions: z.record(z.string(), z.unknown()).nullable().optional(),
270
+ objIndex: z.number().nullable().optional(),
271
+ creatorName: z.string().nullable().optional(),
272
+ channelId: z.string(),
273
+ recipients: z.array(z.number()).nullable().optional(),
274
+ groups: z.array(z.string()).nullable().optional(),
275
+ toEmails: z.array(z.string()).nullable().optional(),
276
+ deleted: z.boolean().nullable().optional(),
277
+ deletedBy: z.number().nullable().optional(),
278
+ version: z.number().nullable().optional(),
279
+ actions: z.array(z.unknown()).nullable().optional(),
280
+ })
281
+ .transform((data) => ({
282
+ ...data,
283
+ url: getFullCommsURL({
284
+ workspaceId: data.workspaceId,
285
+ channelId: data.channelId,
286
+ threadId: data.threadId,
287
+ commentId: data.id,
288
+ }),
289
+ }));
290
+ export const WorkspaceUserSchema = BaseUserSchema.extend({
291
+ email: z.string().nullable().optional(),
292
+ userType: z.enum(USER_TYPES),
293
+ dateFormat: z.string().nullable().optional(),
294
+ theme: z.number().int().nullable().optional(),
295
+ timeFormat: z.string().nullable().optional(),
296
+ version: z.number(),
297
+ });
298
+ // ConversationMessage entity from API. `id` is widened to `string | number`
299
+ // (coerced to a string post-parse) because the backend currently emits
300
+ // either shape depending on the endpoint; the URL/reaction helpers accept
301
+ // both.
302
+ export const ConversationMessageSchema = z
303
+ .object({
304
+ id: z.union([z.string(), z.number()]).transform(String),
305
+ content: z.string(),
306
+ creator: z.number(),
307
+ conversationId: z.string(),
308
+ posted: z.date(),
309
+ systemMessage: SystemMessageSchema,
310
+ attachments: z.array(AttachmentSchema).nullable().optional(),
311
+ reactions: z.record(z.string(), z.array(z.number())).nullable().optional(),
312
+ actions: z.array(z.unknown()).nullable().optional(),
313
+ objIndex: z.number().nullable().optional(),
314
+ lastEdited: z.date().nullable().optional(),
315
+ isDeleted: z.boolean().nullable().optional(),
316
+ directGroupMentions: z.array(z.string()).nullable().optional(),
317
+ directMentions: z.array(z.number()).nullable().optional(),
318
+ version: z.number().nullable().optional(),
319
+ workspaceId: z.number(),
320
+ })
321
+ .transform((data) => ({
322
+ ...data,
323
+ url: getFullCommsURL({
324
+ workspaceId: data.workspaceId,
325
+ conversationId: data.conversationId,
326
+ messageId: data.id,
327
+ }),
328
+ }));
329
+ // InboxThread entity from API - returns full Thread objects with additional inbox metadata.
330
+ export const InboxThreadSchema = z
331
+ .object({
332
+ id: z.string(),
333
+ title: z.string(),
334
+ content: z.string(),
335
+ creator: z.number(),
336
+ creatorName: z.string().nullable().optional(),
337
+ channelId: z.string(),
338
+ workspaceId: z.number(),
339
+ actions: z.array(z.unknown()).nullable().optional(),
340
+ attachments: z.array(AttachmentSchema).nullable().optional(),
341
+ commentCount: z.number(),
342
+ directGroupMentions: z.array(z.string()).nullable().optional(),
343
+ directMentions: z.array(z.number()).nullable().optional(),
344
+ groups: z.array(z.string()).nullable().optional(),
345
+ lastEdited: z.date().nullable().optional(),
346
+ lastObjIndex: z.number().nullable().optional(),
347
+ lastUpdated: z.date(),
348
+ mutedUntil: z.date().nullable().optional(),
349
+ participants: z.array(z.number()).nullable().optional(),
350
+ // Backend wire shape only includes `pinned_ts` (epoch ms or null);
351
+ // derive `pinned` from `pinnedTs != null` if you need a bool.
352
+ pinned: z.boolean().optional(),
353
+ pinnedTs: z.number().int().nullable().optional(),
354
+ posted: z.date(),
355
+ reactions: z.record(z.string(), z.array(z.number())).nullable().optional(),
356
+ recipients: z.array(z.number()).nullable().optional(),
357
+ snippet: z.string(),
358
+ snippetCreator: z.number(),
359
+ snippetMaskAvatarUrl: z.string().nullable().optional(),
360
+ snippetMaskPoster: z.string().nullable().optional(),
361
+ systemMessage: SystemMessageSchema,
362
+ isArchived: z.boolean(),
363
+ inInbox: z.boolean(),
364
+ isSaved: z.boolean().nullable().optional(),
365
+ closed: z.boolean(),
366
+ responders: z.array(z.number()).nullable().optional(),
367
+ lastComment: CommentSchema.nullable().optional(),
368
+ toEmails: z.array(z.string()).nullable().optional(),
369
+ version: z.number().nullable().optional(),
370
+ })
371
+ .transform((data) => ({
372
+ ...data,
373
+ url: getFullCommsURL({
374
+ workspaceId: data.workspaceId,
375
+ channelId: data.channelId,
376
+ threadId: data.id,
377
+ }),
378
+ }));
379
+ // UnreadThread entity from API - simplified thread reference.
380
+ export const UnreadThreadSchema = z.object({
381
+ threadId: z.string(),
382
+ channelId: z.string(),
383
+ objIndex: z.number(),
384
+ directMention: z.boolean(),
385
+ });
386
+ // UnreadConversation entity from API - simplified conversation reference.
387
+ export const UnreadConversationSchema = z.object({
388
+ conversationId: z.string(),
389
+ objIndex: z.number(),
390
+ directMention: z.boolean(),
391
+ });
392
+ // SearchResult entity from API.
393
+ export const SEARCH_RESULT_TYPES = ['thread', 'comment', 'message', 'conversation'];
394
+ export const SearchResultSchema = z.object({
395
+ id: z.string(),
396
+ type: z.enum(SEARCH_RESULT_TYPES),
397
+ snippet: z.string(),
398
+ snippetCreatorId: z.number(),
399
+ snippetLastUpdated: z.date(),
400
+ threadId: z.string().nullable().optional(),
401
+ conversationId: z.string().nullable().optional(),
402
+ commentId: z.string().nullable().optional(),
403
+ channelId: z.string().nullable().optional(),
404
+ channelName: z.string().nullable().optional(),
405
+ channelColor: z.number().nullable().optional(),
406
+ title: z.string().nullable().optional(),
407
+ closed: z.boolean().nullable().optional(),
408
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Marker constants for the two "broadcast" group recipients. These appear
3
+ * in group-bearing fields (`Thread.groups`, `Comment.groups`,
4
+ * `Channel.defaultGroups`, `directGroupMentions`, etc.) in place of a
5
+ * real group ID and tell the backend to notify "everyone in the channel"
6
+ * or "everyone in the thread" respectively. Input and output are
7
+ * symmetric. Defined here (and not in `./entities`) so that `enums`
8
+ * stays leaf-level — `entities` imports from `enums`, never the reverse.
9
+ */
10
+ export const EVERYONE = 'EVERYONE';
11
+ export const EVERYONE_IN_THREAD = 'EVERYONE_IN_THREAD';
12
+ /** Union of the two broadcast group markers. */
13
+ export const GROUP_ID_MARKERS = [EVERYONE, EVERYONE_IN_THREAD];
14
+ // User types for workspace users
15
+ export const USER_TYPES = ['USER', 'GUEST', 'ADMIN'];
16
+ // Workspace plans. The known values today are `'free'`, `'unlimited'`,
17
+ // and `'business'`, but the backend can introduce new plan names without
18
+ // coordinating a SDK release — so the schema accepts any string and the
19
+ // const array stays as a hint for autocomplete only.
20
+ export const WORKSPACE_PLANS = ['free', 'unlimited', 'business'];
21
+ // Audiences that comment-creating endpoints can target alongside (or instead
22
+ // of) individual `recipients` / custom `groups`.
23
+ export const NOTIFY_AUDIENCES = ['channel', 'thread'];
24
+ /**
25
+ * Internal mapping from {@link NotifyAudience} to the broadcast marker IDs
26
+ * (`EVERYONE` / `EVERYONE_IN_THREAD`) that comment- and thread-creation
27
+ * endpoints use on the wire. SDK consumers should use {@link NotifyAudience}
28
+ * via `notifyAudience` on the request args rather than passing these IDs
29
+ * directly.
30
+ */
31
+ export const NOTIFY_AUDIENCE_GROUP_IDS = {
32
+ channel: EVERYONE,
33
+ thread: EVERYONE_IN_THREAD,
34
+ };
@@ -0,0 +1,8 @@
1
+ import { CustomError } from 'ts-custom-error';
2
+ export class CommsRequestError extends CustomError {
3
+ constructor(message, httpStatusCode, responseData) {
4
+ super(message);
5
+ this.httpStatusCode = httpStatusCode;
6
+ this.responseData = responseData;
7
+ }
8
+ }
@@ -0,0 +1 @@
1
+ export const HTTP_METHODS = ['POST', 'GET', 'PUT', 'PATCH', 'DELETE'];
@@ -0,0 +1,5 @@
1
+ export * from './entities.js';
2
+ export * from './enums.js';
3
+ export * from './errors.js';
4
+ export * from './http.js';
5
+ export * from './requests.js';
@@ -0,0 +1,114 @@
1
+ import { z } from 'zod';
2
+ import { AttachmentSchema } from './entities.js';
3
+ import { NOTIFY_AUDIENCES } from './enums.js';
4
+ /**
5
+ * Create-side requests require an `id` on the wire. The SDK clients accept
6
+ * `id` as optional and auto-generate one via
7
+ * {@link import('../utils/uuidv7').generateId} when the caller doesn't pass
8
+ * one — callers can still mint their own ID locally (e.g. for optimistic
9
+ * UI) and have it stick.
10
+ */
11
+ export const CreateChannelArgsSchema = z.object({
12
+ workspaceId: z.number(),
13
+ name: z.string(),
14
+ id: z.string().optional(),
15
+ userIds: z.array(z.number()).nullable().optional(),
16
+ color: z.number().nullable().optional(),
17
+ public: z.boolean().nullable().optional(),
18
+ description: z.string().nullable().optional(),
19
+ defaultGroups: z.array(z.string()).nullable().optional(),
20
+ defaultRecipients: z.array(z.number()).nullable().optional(),
21
+ isFavorited: z.boolean().nullable().optional(),
22
+ icon: z.number().nullable().optional(),
23
+ });
24
+ export const UpdateChannelArgsSchema = z.object({
25
+ id: z.string(),
26
+ name: z.string(),
27
+ color: z.number().nullable().optional(),
28
+ public: z.boolean().nullable().optional(),
29
+ description: z.string().nullable().optional(),
30
+ defaultGroups: z.array(z.string()).nullable().optional(),
31
+ defaultRecipients: z.array(z.number()).nullable().optional(),
32
+ isFavorited: z.boolean().nullable().optional(),
33
+ icon: z.number().nullable().optional(),
34
+ });
35
+ export const CreateThreadArgsSchema = z.object({
36
+ channelId: z.string(),
37
+ content: z.string(),
38
+ title: z.string().nullable().optional(),
39
+ id: z.string().optional(),
40
+ recipients: z.array(z.number()).nullable().optional(),
41
+ groups: z.array(z.string()).nullable().optional(),
42
+ });
43
+ export const UpdateThreadArgsSchema = z.object({
44
+ id: z.string(),
45
+ title: z.string().nullable().optional(),
46
+ content: z.string().nullable().optional(),
47
+ });
48
+ export const CreateCommentArgsSchema = z.object({
49
+ threadId: z.string(),
50
+ content: z.string(),
51
+ id: z.string().optional(),
52
+ attachments: z.array(AttachmentSchema).nullable().optional(),
53
+ actions: z.unknown().nullable().optional(),
54
+ recipients: z.array(z.number()).nullable().optional(),
55
+ groups: z.array(z.string()).nullable().optional(),
56
+ directMentions: z.array(z.number()).nullable().optional(),
57
+ directGroupMentions: z.array(z.string()).nullable().optional(),
58
+ notifyAudience: z.enum(NOTIFY_AUDIENCES).nullable().optional(),
59
+ });
60
+ export const UpdateCommentArgsSchema = z.object({
61
+ id: z.string(),
62
+ content: z.string(),
63
+ });
64
+ export const CreateConversationArgsSchema = z.object({
65
+ workspaceId: z.number(),
66
+ recipients: z.array(z.number()),
67
+ id: z.string().optional(),
68
+ title: z.string().nullable().optional(),
69
+ });
70
+ export const CreateMessageArgsSchema = z
71
+ .object({
72
+ conversationId: z.string().nullable().optional(),
73
+ threadId: z.string().nullable().optional(),
74
+ content: z.string(),
75
+ id: z.string().optional(),
76
+ attachments: z.array(AttachmentSchema).nullable().optional(),
77
+ })
78
+ .refine((data) => {
79
+ return ((data.conversationId && !data.threadId) || (!data.conversationId && data.threadId));
80
+ }, {
81
+ message: 'Exactly one of conversationId or threadId must be provided',
82
+ });
83
+ export const GetChannelsArgsSchema = z.object({
84
+ workspaceId: z.number(),
85
+ archived: z.boolean().nullable().optional(),
86
+ });
87
+ export const GetThreadsArgsSchema = z.object({
88
+ workspaceId: z.number(),
89
+ channelId: z.string().nullable().optional(),
90
+ archived: z.boolean().nullable().optional(),
91
+ newerThan: z.date().nullable().optional(),
92
+ olderThan: z.date().nullable().optional(),
93
+ limit: z.number().nullable().optional(),
94
+ });
95
+ export const GetCommentsArgsSchema = z.object({
96
+ threadId: z.string(),
97
+ from: z.date().nullable().optional(),
98
+ newerThan: z.date().nullable().optional(),
99
+ olderThan: z.date().nullable().optional(),
100
+ limit: z.number().nullable().optional(),
101
+ });
102
+ export const GetConversationsArgsSchema = z.object({
103
+ workspaceId: z.number(),
104
+ archived: z.boolean().nullable().optional(),
105
+ });
106
+ export const GetOrCreateConversationArgsSchema = z.object({
107
+ workspaceId: z.number(),
108
+ userIds: z.array(z.number()),
109
+ id: z.string().optional(),
110
+ });
111
+ // Inbox
112
+ export const ARCHIVE_FILTER_VALUES = ['active', 'archived', 'all'];
113
+ // Threads
114
+ export const THREAD_ACTIONS = ['close', 'reopen'];
@@ -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,3 @@
1
+ export * from './case-conversion.js';
2
+ export * from './url-helpers.js';
3
+ export * from './uuidv7.js';
@@ -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
+ }