@whitewall/blip-sdk 0.0.135 → 0.0.137

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 (56) hide show
  1. package/dist/cjs/client.js +1 -1
  2. package/dist/cjs/client.js.map +1 -1
  3. package/dist/esm/client.js +1 -1
  4. package/dist/esm/client.js.map +1 -1
  5. package/dist/types/client.d.ts +2 -2
  6. package/dist/types/client.d.ts.map +1 -1
  7. package/package.json +2 -2
  8. package/src/client.ts +117 -0
  9. package/src/index.ts +6 -0
  10. package/src/namespaces/account.ts +729 -0
  11. package/src/namespaces/activecampaign.ts +285 -0
  12. package/src/namespaces/analytics.ts +230 -0
  13. package/src/namespaces/billing.ts +17 -0
  14. package/src/namespaces/builder.ts +52 -0
  15. package/src/namespaces/configurations.ts +19 -0
  16. package/src/namespaces/context.ts +67 -0
  17. package/src/namespaces/desk.ts +679 -0
  18. package/src/namespaces/media.ts +39 -0
  19. package/src/namespaces/namespace.ts +125 -0
  20. package/src/namespaces/plugins.ts +223 -0
  21. package/src/namespaces/portal.ts +402 -0
  22. package/src/namespaces/scheduler.ts +88 -0
  23. package/src/namespaces/whatsapp.ts +383 -0
  24. package/src/sender/bliperror.ts +42 -0
  25. package/src/sender/enveloperesolver.ts +148 -0
  26. package/src/sender/gateway/customgatewaysender.ts +43 -0
  27. package/src/sender/http/httpsender.ts +94 -0
  28. package/src/sender/index.ts +7 -0
  29. package/src/sender/plugin/communication.ts +72 -0
  30. package/src/sender/plugin/pluginsender.ts +75 -0
  31. package/src/sender/security.ts +33 -0
  32. package/src/sender/sender.ts +145 -0
  33. package/src/sender/sessionnegotiator.ts +175 -0
  34. package/src/sender/tcp/tcpsender.ts +252 -0
  35. package/src/sender/throttler.ts +36 -0
  36. package/src/sender/websocket/websocketsender.ts +175 -0
  37. package/src/types/account.ts +84 -0
  38. package/src/types/analytics.ts +18 -0
  39. package/src/types/billing.ts +15 -0
  40. package/src/types/command.ts +47 -0
  41. package/src/types/commons.ts +16 -0
  42. package/src/types/desk.ts +51 -0
  43. package/src/types/envelope.ts +9 -0
  44. package/src/types/flow.ts +327 -0
  45. package/src/types/index.ts +13 -0
  46. package/src/types/message.ts +116 -0
  47. package/src/types/node.ts +86 -0
  48. package/src/types/notification.ts +18 -0
  49. package/src/types/plugins.ts +51 -0
  50. package/src/types/portal.ts +39 -0
  51. package/src/types/reason.ts +22 -0
  52. package/src/types/session.ts +22 -0
  53. package/src/types/whatsapp.ts +84 -0
  54. package/src/utils/odata.ts +114 -0
  55. package/src/utils/random.ts +3 -0
  56. package/src/utils/uri.ts +46 -0
@@ -0,0 +1,383 @@
1
+ import type { BlipClient } from '../client.ts'
2
+ import type { Identity } from '../types/index.ts'
3
+ import type {
4
+ BodyComponent,
5
+ ButtonComponent,
6
+ HandleFormats,
7
+ HeaderComponent,
8
+ MessageTemplate,
9
+ MessageTemplateVariable,
10
+ WhatsappFlow,
11
+ } from '../types/whatsapp.ts'
12
+ import { uri } from '../utils/uri.ts'
13
+ import { type ConsumeOptions, Namespace, type SendCommandOptions } from './namespace.ts'
14
+
15
+ export class WhatsAppNamespace extends Namespace {
16
+ constructor(blipClient: BlipClient, defaultOptions?: SendCommandOptions) {
17
+ super(blipClient, 'wa.gw', defaultOptions)
18
+ }
19
+
20
+ public async phoneToIdentity(phoneNumber: string, opts?: ConsumeOptions): Promise<Identity> {
21
+ const fixedPhoneNumber = phoneNumber[0] !== '+' ? `+${phoneNumber}` : phoneNumber
22
+
23
+ try {
24
+ const result = await this.sendCommand<
25
+ 'get',
26
+ {
27
+ alternativeAccount: Identity
28
+ }
29
+ >(
30
+ {
31
+ method: 'get',
32
+ uri: uri`/accounts/${fixedPhoneNumber}`,
33
+ },
34
+ opts,
35
+ )
36
+
37
+ return result.alternativeAccount
38
+ } catch {
39
+ return `${phoneNumber.replace('+', '')}@wa.gw.msging.net`
40
+ }
41
+ }
42
+
43
+ /**
44
+ * This is a heavily cached endpoint on Meta, so it may take a while to reflect changes
45
+ * @param filters.status - Filter by status of the message template
46
+ * @param filters.name - Filter by name of the message template, supports partial match
47
+ */
48
+ public async getMessageTemplates(
49
+ filters?: Partial<{
50
+ status: MessageTemplate['status']
51
+ name: string
52
+ }>,
53
+ opts?: ConsumeOptions,
54
+ ): Promise<Array<MessageTemplate>> {
55
+ return await this.sendCommand(
56
+ {
57
+ method: 'get',
58
+ uri: uri`/message-templates?${{ status: filters?.status?.toUpperCase(), templateName: filters?.name }}`,
59
+ },
60
+ {
61
+ collection: true,
62
+ ...opts,
63
+ },
64
+ )
65
+ }
66
+
67
+ public async getMessageTemplate(
68
+ templateName: string,
69
+ language?: string,
70
+ opts?: ConsumeOptions,
71
+ ): Promise<(MessageTemplate & { variables: Array<MessageTemplateVariable> }) | undefined> {
72
+ let templates = await this.getMessageTemplates({ name: templateName }, opts)
73
+ if (language) {
74
+ templates = templates.filter((template) => template.language === language)
75
+ }
76
+ const template = templates.find((template) => template.name === templateName)
77
+ if (!template) {
78
+ return
79
+ }
80
+
81
+ const variables: Array<MessageTemplateVariable> = []
82
+
83
+ const handleHeader = template.components.find(
84
+ (c): c is HeaderComponent<HandleFormats> =>
85
+ c.type === 'HEADER' && ['IMAGE', 'VIDEO', 'DOCUMENT'].includes(c.format),
86
+ )
87
+ if (handleHeader) {
88
+ variables.push({ type: 'MEDIA', example: handleHeader.example.header_handle?.[0] })
89
+ }
90
+
91
+ const body = template.components.find((c): c is BodyComponent => c.type === 'BODY')
92
+ if (body?.example?.body_text) {
93
+ variables.push(...body.example.body_text[0].map((text) => ({ type: 'BODY' as const, example: text })))
94
+ } else if (body) {
95
+ variables.push(...(body.text.match(/{{(\d)}}/g)?.map(() => ({ type: 'BODY' as const })) ?? []))
96
+ }
97
+
98
+ const button = template.components.find((c): c is ButtonComponent => c.type === 'BUTTONS')
99
+ if (button) {
100
+ const urlButton = button.buttons.find((b) => b.type === 'URL')
101
+ if (urlButton?.example?.length) {
102
+ variables.push(...urlButton.example.map((example: string) => ({ type: 'BUTTON', example })))
103
+ } else if (urlButton) {
104
+ variables.push(...(urlButton.url.match(/{{(\d)}}/g)?.map(() => ({ type: 'BUTTON' })) ?? []))
105
+ }
106
+ }
107
+
108
+ return {
109
+ ...template,
110
+ variables,
111
+ }
112
+ }
113
+
114
+ public async createMessageTemplateAttachment(
115
+ url: string,
116
+ opts?: ConsumeOptions,
117
+ ): Promise<{
118
+ uploadSessionId: string
119
+ fileHandle: string
120
+ fileSize: string
121
+ fileSizeUploaded: string
122
+ }> {
123
+ return await this.sendCommand(
124
+ {
125
+ method: 'set',
126
+ uri: uri`/message-templates-attachment`,
127
+ type: 'application/vnd.lime.media-link+json',
128
+ resource: {
129
+ uri: url,
130
+ },
131
+ },
132
+ opts,
133
+ )
134
+ }
135
+
136
+ public async createMessageTemplate(
137
+ template: Pick<MessageTemplate, 'name' | 'category' | 'language' | 'components'>,
138
+ opts?: ConsumeOptions,
139
+ ): Promise<void> {
140
+ return await this.sendCommand(
141
+ {
142
+ method: 'set',
143
+ uri: uri`/message-templates`,
144
+ type: 'application/json',
145
+ resource: template,
146
+ },
147
+ opts,
148
+ )
149
+ }
150
+
151
+ public async createMessageTemplateAndPoll(
152
+ template: Pick<MessageTemplate, 'name' | 'category' | 'language' | 'components'>,
153
+ opts?: ConsumeOptions,
154
+ ): Promise<MessageTemplate> {
155
+ await this.sendCommand(
156
+ {
157
+ method: 'set',
158
+ uri: uri`/message-templates`,
159
+ type: 'application/json',
160
+ resource: template,
161
+ },
162
+ opts,
163
+ )
164
+
165
+ while (true) {
166
+ await new Promise((resolve) => setTimeout(resolve, 3000))
167
+
168
+ const created = await this.getMessageTemplate(template.name, template.language, opts)
169
+ if (!created) {
170
+ throw new Error(`Template ${template.name} was not created`)
171
+ }
172
+
173
+ if (created.status !== 'PENDING') {
174
+ if (created.status === 'APPROVED') {
175
+ return created
176
+ }
177
+
178
+ throw new Error(`Template ${template.name} was rejected: ${created.rejected_reason}`)
179
+ }
180
+ }
181
+ }
182
+
183
+ public async deleteMessageTemplate(templateName: string, opts?: ConsumeOptions): Promise<void> {
184
+ return await this.sendCommand(
185
+ {
186
+ method: 'delete',
187
+ uri: uri`/message-templates/${templateName}`,
188
+ },
189
+ opts,
190
+ )
191
+ }
192
+
193
+ public async createFlow(
194
+ flow: { name: string; category: string; endpointUri?: string },
195
+ opts?: ConsumeOptions,
196
+ ): Promise<{ id: string }> {
197
+ return await this.sendCommand(
198
+ {
199
+ method: 'set',
200
+ uri: uri`/whatsapp-flows`,
201
+ type: 'application/json',
202
+ resource: {
203
+ name: flow.name,
204
+ categories: [flow.category],
205
+ endpoint_uri: flow.endpointUri,
206
+ },
207
+ },
208
+ opts,
209
+ )
210
+ }
211
+
212
+ public async updateFlow(
213
+ id: string,
214
+ flow: { name?: string; endpointUri?: string },
215
+ opts?: ConsumeOptions,
216
+ ): Promise<void> {
217
+ return await this.sendCommand(
218
+ {
219
+ method: 'set',
220
+ uri: uri`/whatsapp-flows/${id}`,
221
+ type: 'application/json',
222
+ resource: {
223
+ name: flow.name,
224
+ endpoint_uri: flow.endpointUri,
225
+ },
226
+ },
227
+ opts,
228
+ )
229
+ }
230
+
231
+ public async updateFlowJson(id: string, json: string, opts?: ConsumeOptions): Promise<void> {
232
+ return await this.sendCommand(
233
+ {
234
+ method: 'set',
235
+ uri: uri`/whatsapp-flows/flow-json/${id}`,
236
+ type: 'application/json',
237
+ resource: JSON.parse(json),
238
+ },
239
+ opts,
240
+ )
241
+ }
242
+
243
+ public async updateFlowsPublicKey(publicKey: string, opts?: ConsumeOptions): Promise<void> {
244
+ return await this.sendCommand(
245
+ {
246
+ method: 'set',
247
+ uri: uri`/whatsapp-flows/public-key/upload`,
248
+ type: 'application/json',
249
+ resource: {
250
+ business_public_key: publicKey,
251
+ },
252
+ },
253
+ opts,
254
+ )
255
+ }
256
+
257
+ public async getFlows(opts?: ConsumeOptions): Promise<Array<WhatsappFlow>> {
258
+ return await this.sendCommand(
259
+ {
260
+ method: 'get',
261
+ uri: uri`/whatsapp-flows`,
262
+ },
263
+ {
264
+ collection: true,
265
+ ...opts,
266
+ },
267
+ )
268
+ }
269
+
270
+ public async getFlow(id: string, opts?: ConsumeOptions): Promise<WhatsappFlow> {
271
+ return await this.sendCommand(
272
+ {
273
+ method: 'get',
274
+ uri: uri`/whatsapp-flows/${id}`,
275
+ },
276
+ opts,
277
+ )
278
+ }
279
+
280
+ public async publishFlow(id: string, opts?: ConsumeOptions): Promise<void> {
281
+ return await this.sendCommand(
282
+ {
283
+ method: 'get',
284
+ uri: uri`/whatsapp-flows/publish/${id}`,
285
+ },
286
+ opts,
287
+ )
288
+ }
289
+
290
+ public async deprecateFlow(id: string, opts?: ConsumeOptions): Promise<void> {
291
+ return await this.sendCommand(
292
+ {
293
+ method: 'get',
294
+ uri: uri`/whatsapp-flows/deprecate/${id}`,
295
+ },
296
+ opts,
297
+ )
298
+ }
299
+
300
+ public async getFlowAssets(
301
+ id: string,
302
+ opts?: ConsumeOptions,
303
+ ): Promise<Array<{ name: string; download_url: string }>> {
304
+ return await this.sendCommand(
305
+ {
306
+ method: 'get',
307
+ uri: uri`/whatsapp-flows/assets/${id}`,
308
+ },
309
+ {
310
+ collection: true,
311
+ ...opts,
312
+ },
313
+ )
314
+ }
315
+
316
+ public async deleteFlow(id: string, opts?: ConsumeOptions): Promise<void> {
317
+ return await this.sendCommand(
318
+ {
319
+ method: 'delete',
320
+ uri: uri`/whatsapp-flows/${id}`,
321
+ },
322
+ opts,
323
+ )
324
+ }
325
+
326
+ public async isChannelActive(opts?: ConsumeOptions): Promise<boolean> {
327
+ const configurations = await this.blipClient.account.getConfigurations({
328
+ ...opts,
329
+ ownerIdentity: this.identity,
330
+ })
331
+
332
+ return 'IsChannelActive' in configurations && configurations.IsChannelActive === 'True'
333
+ }
334
+
335
+ public async getWabaDetails(opts?: ConsumeOptions): Promise<{
336
+ id: string
337
+ name: string
338
+ reviewStatus: 'APPROVED' | 'PENDING' | 'REJECTED'
339
+ currency: string
340
+ messageTemplateNamespace: string
341
+ }> {
342
+ return await this.sendCommand(
343
+ {
344
+ method: 'get',
345
+ uri: uri`/wabas/details`,
346
+ },
347
+ opts,
348
+ )
349
+ }
350
+
351
+ public async getPhoneNumberDetails(opts?: ConsumeOptions): Promise<{
352
+ id: string
353
+ account_mode: 'SANDBOX' | 'LIVE'
354
+ name_status: 'APPROVED' | 'DECLINED' | 'EXPIRED' | 'PENDING_REVIEW' | 'NONE'
355
+ new_name_status: 'APPROVED' | 'DECLINED' | 'EXPIRED' | 'PENDING_REVIEW' | 'NONE'
356
+ verified_name: string
357
+ display_phone_number: string
358
+ quality_rating: 'GREEN' | 'YELLOW' | 'RED' | 'UNKNOWN'
359
+ quality_score: {
360
+ score: 'GREEN' | 'YELLOW' | 'RED' | 'UNKNOWN'
361
+ }
362
+ }> {
363
+ return await this.sendCommand(
364
+ {
365
+ method: 'get',
366
+ uri: uri`/phone-number-details`,
367
+ },
368
+ opts,
369
+ )
370
+ }
371
+
372
+ public async getPhoneNumber(opts?: ConsumeOptions): Promise<string> {
373
+ const configurations = await this.blipClient.account.getConfigurations({
374
+ ...opts,
375
+ ownerIdentity: this.identity,
376
+ })
377
+ if ('PhoneNumber' in configurations && 'CountryCode' in configurations) {
378
+ return `+${configurations.CountryCode}${configurations.PhoneNumber}`
379
+ }
380
+
381
+ throw new Error('Phone number not found')
382
+ }
383
+ }
@@ -0,0 +1,42 @@
1
+ import type { CommandMethods, CommandResponse } from '../types/command.ts'
2
+ import type { Reason } from '../types/reason.ts'
3
+ import { ReasonCodes } from '../types/reason.ts'
4
+
5
+ export class BlipError extends Error {
6
+ constructor(
7
+ public readonly uri: string,
8
+ public readonly cause: Reason,
9
+ public commandResponse?: CommandResponse<unknown, CommandMethods, 'failure'>,
10
+ ) {
11
+ super(`Blip command for ${uri} failed (${cause.code}): ${cause.description}`)
12
+ }
13
+
14
+ public get code(): number {
15
+ return this.cause.code
16
+ }
17
+
18
+ public static isFailedCommandResponse(
19
+ maybeCommandResponse: unknown,
20
+ ): maybeCommandResponse is CommandResponse<unknown, CommandMethods, 'failure'> {
21
+ return (
22
+ typeof maybeCommandResponse === 'object' &&
23
+ maybeCommandResponse !== null &&
24
+ 'status' in maybeCommandResponse &&
25
+ maybeCommandResponse.status === 'failure'
26
+ )
27
+ }
28
+
29
+ public static commandResponseToBlipError(
30
+ uri: string,
31
+ commandResponse: CommandResponse<unknown, CommandMethods, 'failure'>,
32
+ ): BlipError {
33
+ return new BlipError(uri, commandResponse.reason, commandResponse)
34
+ }
35
+
36
+ static retryableBlipErrors = [
37
+ ReasonCodes.PipelineTimeout,
38
+ ReasonCodes.MaxThroughputExceeded,
39
+ ReasonCodes.GeneralError,
40
+ ReasonCodes.FailedToSendEnvelopeViaHTTP,
41
+ ]
42
+ }
@@ -0,0 +1,148 @@
1
+ import {
2
+ type CommonNotification,
3
+ type Envelope,
4
+ type FailedNotification,
5
+ type JsonObject,
6
+ type MaybePromise,
7
+ Node,
8
+ type Notification,
9
+ type UnknownCommand,
10
+ type UnknownMessage,
11
+ isCommand,
12
+ isMessage,
13
+ isNotification,
14
+ } from '../types/index.ts'
15
+ import { ReasonCodes } from '../types/reason.ts'
16
+ import type { OpenConnectionSender } from './sender.ts'
17
+
18
+ export type EventMap = {
19
+ message: (message: UnknownMessage) => MaybePromise<void>
20
+ command: (command: UnknownCommand) => MaybePromise<string | JsonObject> | MaybePromise<void>
21
+ notification: (notification: Notification) => MaybePromise<void>
22
+ }
23
+
24
+ export type Listener<K extends keyof EventMap> = {
25
+ predicate?: (...args: Parameters<EventMap[K]>) => boolean
26
+ callback: (...args: Parameters<EventMap[K]>) => ReturnType<EventMap[K]>
27
+ }
28
+
29
+ type Listeners = {
30
+ [K in keyof EventMap]: Array<Listener<K>>
31
+ }
32
+
33
+ export class EnvelopeResolver {
34
+ private readonly listeners: Listeners = {
35
+ message: [],
36
+ command: [],
37
+ notification: [],
38
+ }
39
+
40
+ private readonly waitingEnvelopeResponseResolvers: Record<string, (response: Envelope) => void> = {}
41
+
42
+ constructor(private readonly sender: OpenConnectionSender) {}
43
+
44
+ public async resolve(envelope: Envelope) {
45
+ const deferred = this.waitingEnvelopeResponseResolvers[envelope.id]
46
+ if (deferred) {
47
+ delete this.waitingEnvelopeResponseResolvers[envelope.id]
48
+ deferred(envelope)
49
+ } else if (isCommand(envelope)) {
50
+ if (envelope.method === 'get' && envelope.uri === '/ping') {
51
+ await this.sender.sendCommandResponse({
52
+ id: envelope.id,
53
+ to: envelope.from,
54
+ method: 'get',
55
+ type: 'application/vnd.lime.ping+json',
56
+ status: 'success',
57
+ resource: {},
58
+ })
59
+ } else {
60
+ try {
61
+ const response = await this.emit('command', envelope)
62
+ const type = typeof response === 'string' ? 'text/plain' : response ? 'application/json' : undefined
63
+ const resource = typeof response === 'string' || response ? response : undefined
64
+ await this.sender.sendCommandResponse({
65
+ id: envelope.id,
66
+ method: envelope.method,
67
+ to: envelope.from,
68
+ status: 'success',
69
+ type,
70
+ resource,
71
+ })
72
+ } catch (err) {
73
+ await this.sender.sendCommandResponse({
74
+ id: envelope.id,
75
+ method: envelope.method,
76
+ to: envelope.from,
77
+ status: 'failure',
78
+ reason: {
79
+ code: ReasonCodes.GeneralError,
80
+ description: err instanceof Error ? err.message : String(err),
81
+ },
82
+ })
83
+ }
84
+ }
85
+ } else if (isMessage(envelope)) {
86
+ const shouldNotify =
87
+ !envelope.to || this.sender.session?.localNode.toIdentity() === Node.from(envelope.to).toIdentity()
88
+ const notify = async (notification: CommonNotification | FailedNotification) => {
89
+ if (shouldNotify) {
90
+ await this.sender.sendNotification({
91
+ ...notification,
92
+ id: envelope.id,
93
+ to: notification.event === 'failed' ? envelope.from : (envelope.pp ?? envelope.from),
94
+ })
95
+ }
96
+ }
97
+
98
+ await notify({ event: 'received' })
99
+ try {
100
+ await this.emit('message', envelope)
101
+ await notify({ event: 'consumed' })
102
+ } catch (err) {
103
+ await notify({
104
+ event: 'failed',
105
+ reason: {
106
+ code: ReasonCodes.GeneralError,
107
+ description: err instanceof Error ? err.message : String(err),
108
+ },
109
+ })
110
+ }
111
+ } else if (isNotification(envelope)) {
112
+ await this.emit('notification', envelope)
113
+ }
114
+ }
115
+
116
+ public createEnvelopeResponsePromise(id: string): Promise<Envelope> {
117
+ return new Promise((resolve) => {
118
+ this.waitingEnvelopeResponseResolvers[id] = resolve
119
+ })
120
+ }
121
+
122
+ public addListener<K extends keyof EventMap>(ev: K, listener: Listener<K>) {
123
+ if (this.listeners[ev].some((l) => l.callback === listener.callback)) {
124
+ throw new Error(`Listener for '${ev}' already exists.`)
125
+ }
126
+
127
+ this.listeners[ev].push(listener)
128
+ }
129
+
130
+ public removeListener<K extends keyof EventMap>(ev: K, callback: Listener<K>['callback']) {
131
+ this.listeners[ev] = this.listeners[ev].filter((l) => l.callback !== callback) as Listeners[K]
132
+ }
133
+
134
+ private async emit<K extends keyof EventMap>(
135
+ ev: K,
136
+ ...args: Parameters<EventMap[K]>
137
+ ): Promise<unknown | undefined> {
138
+ for (const listener of this.listeners[ev]) {
139
+ if (!listener.predicate || listener.predicate(...args)) {
140
+ const result = await listener.callback(...args)
141
+ if (result !== undefined) {
142
+ return result
143
+ }
144
+ }
145
+ }
146
+ return undefined
147
+ }
148
+ }
@@ -0,0 +1,43 @@
1
+ import type { Message, MessageTypesContent } from '../../types/index.ts'
2
+ import { ConnectionSender, type ConnectionSenderConstructor, type Sender } from '../sender.ts'
3
+
4
+ export class CustomGatewayHttpSender extends ConnectionSender implements Sender {
5
+ private readonly baseurl: string
6
+ private readonly token: string
7
+
8
+ constructor(options: ConstructorParameters<ConnectionSenderConstructor>[0]) {
9
+ super(options)
10
+
11
+ if (options.authentication.scheme === 'plain') {
12
+ this.token = CustomGatewayHttpSender.createToken(options.node, btoa(options.authentication.password))
13
+ } else if (options.authentication.scheme === 'token') {
14
+ this.token = options.authentication.token
15
+ } else {
16
+ throw new Error('CustomGatewayHttpSender only supports plain or token authentication')
17
+ }
18
+
19
+ const prefix = options.tenantId ? `${options.tenantId}.` : ''
20
+ this.baseurl = `https://${prefix}custom.gw.${this.domain}`
21
+ }
22
+
23
+ public async sendMessage<Type extends keyof MessageTypesContent>(message: Message<Type>): Promise<void> {
24
+ const response = await fetch(`${this.baseurl}/messages`, {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ Authorization: `Basic ${this.token}`,
29
+ },
30
+ body: JSON.stringify(message),
31
+ })
32
+
33
+ if (!response.ok) {
34
+ throw new Error(`Failed to send message: ${response.statusText}`)
35
+ }
36
+ }
37
+
38
+ sendCommand(): Promise<unknown> {
39
+ throw new Error('Custom gateway clients cannot send commands')
40
+ }
41
+
42
+ public static login = ConnectionSender.login<CustomGatewayHttpSender>
43
+ }