@whitewall/blip-sdk 0.0.185 → 0.0.187

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 (52) hide show
  1. package/dist/cjs/namespaces/namespace.js +2 -0
  2. package/dist/cjs/namespaces/namespace.js.map +1 -1
  3. package/dist/cjs/namespaces/portal.js +38 -0
  4. package/dist/cjs/namespaces/portal.js.map +1 -1
  5. package/dist/cjs/namespaces/whatsapp.js +29 -0
  6. package/dist/cjs/namespaces/whatsapp.js.map +1 -1
  7. package/dist/cjs/sender/gateway/customgatewaysender.js +1 -1
  8. package/dist/cjs/sender/gateway/customgatewaysender.js.map +1 -1
  9. package/dist/cjs/sender/http/httpsender.js +2 -2
  10. package/dist/cjs/sender/http/httpsender.js.map +1 -1
  11. package/dist/cjs/sender/sender.js +16 -1
  12. package/dist/cjs/sender/sender.js.map +1 -1
  13. package/dist/cjs/sender/tcp/tcpsender.js +2 -2
  14. package/dist/cjs/sender/tcp/tcpsender.js.map +1 -1
  15. package/dist/cjs/sender/websocket/websocketsender.js +2 -2
  16. package/dist/cjs/sender/websocket/websocketsender.js.map +1 -1
  17. package/dist/esm/namespaces/namespace.js +2 -0
  18. package/dist/esm/namespaces/namespace.js.map +1 -1
  19. package/dist/esm/namespaces/portal.js +38 -0
  20. package/dist/esm/namespaces/portal.js.map +1 -1
  21. package/dist/esm/namespaces/whatsapp.js +29 -0
  22. package/dist/esm/namespaces/whatsapp.js.map +1 -1
  23. package/dist/esm/sender/gateway/customgatewaysender.js +1 -1
  24. package/dist/esm/sender/gateway/customgatewaysender.js.map +1 -1
  25. package/dist/esm/sender/http/httpsender.js +2 -2
  26. package/dist/esm/sender/http/httpsender.js.map +1 -1
  27. package/dist/esm/sender/sender.js +16 -1
  28. package/dist/esm/sender/sender.js.map +1 -1
  29. package/dist/esm/sender/tcp/tcpsender.js +2 -2
  30. package/dist/esm/sender/tcp/tcpsender.js.map +1 -1
  31. package/dist/esm/sender/websocket/websocketsender.js +2 -2
  32. package/dist/esm/sender/websocket/websocketsender.js.map +1 -1
  33. package/dist/types/namespaces/namespace.d.ts +1 -0
  34. package/dist/types/namespaces/namespace.d.ts.map +1 -1
  35. package/dist/types/namespaces/portal.d.ts +17 -0
  36. package/dist/types/namespaces/portal.d.ts.map +1 -1
  37. package/dist/types/namespaces/whatsapp.d.ts +28 -0
  38. package/dist/types/namespaces/whatsapp.d.ts.map +1 -1
  39. package/dist/types/sender/sender.d.ts +5 -1
  40. package/dist/types/sender/sender.d.ts.map +1 -1
  41. package/dist/types/types/billing.d.ts +5 -4
  42. package/dist/types/types/billing.d.ts.map +1 -1
  43. package/package.json +5 -5
  44. package/src/namespaces/namespace.ts +3 -0
  45. package/src/namespaces/portal.ts +60 -0
  46. package/src/namespaces/whatsapp.ts +60 -0
  47. package/src/sender/gateway/customgatewaysender.ts +1 -1
  48. package/src/sender/http/httpsender.ts +2 -2
  49. package/src/sender/sender.ts +20 -1
  50. package/src/sender/tcp/tcpsender.ts +2 -2
  51. package/src/sender/websocket/websocketsender.ts +2 -2
  52. package/src/types/billing.ts +5 -4
@@ -2,6 +2,7 @@ import type { BlipClient } from '../client.ts'
2
2
  import { BlipError } from '../sender/bliperror.ts'
3
3
  import { PluginSender } from '../sender/plugin/pluginsender.ts'
4
4
  import type { Tenant } from '../types/account.ts'
5
+ import type { Plan } from '../types/billing.ts'
5
6
  import { type Identity, Node, type PossiblyNode } from '../types/node.ts'
6
7
  import type {
7
8
  Application,
@@ -193,6 +194,27 @@ export class PortalNamespace extends Namespace {
193
194
  })
194
195
  }
195
196
 
197
+ /**
198
+ * Creates the tenant when it doesn't exist (the caller becomes its owner and admin)
199
+ * or updates it when it does (requires the tenant admin role).
200
+ */
201
+ public async setTenant(
202
+ tenantId: string,
203
+ tenant: Pick<Tenant, 'name'> &
204
+ Partial<Pick<Tenant, 'photoUri' | 'isTestContract' | 'creationSource' | 'canViewMobileDesk'>>,
205
+ opts?: ConsumeOptions,
206
+ ): Promise<void> {
207
+ return await this.sendCommand(
208
+ {
209
+ method: 'set',
210
+ uri: uri`/tenants/${tenantId}`,
211
+ type: 'application/vnd.iris.portal.tenant+json',
212
+ resource: { id: tenantId, ...tenant },
213
+ },
214
+ opts,
215
+ )
216
+ }
217
+
196
218
  public async getTenantActiveMessagesMetrics(
197
219
  tenantId: string,
198
220
  interval: 'D' | 'M' | 'NI',
@@ -372,6 +394,44 @@ export class PortalNamespace extends Namespace {
372
394
  )
373
395
  }
374
396
 
397
+ /** Lists all available billing plans. */
398
+ public async getPlans(opts?: ConsumeOptions): Promise<Array<Plan>> {
399
+ return await this.sendCommand(
400
+ {
401
+ method: 'get',
402
+ uri: uri`/plans`,
403
+ },
404
+ {
405
+ collection: true,
406
+ ...opts,
407
+ },
408
+ )
409
+ }
410
+
411
+ /**
412
+ * Subscribes a payment account to a plan, replacing the account's active subscription.
413
+ *
414
+ * **Admin-only**: the server rejects any caller other than `admin@blip.ai`
415
+ *
416
+ * The subscriber is the tenant's `paymentAccount` identity, not the tenant id, see {@link getTenant}.
417
+ * Reads such as {@link getTenantSubscription} may briefly return the previous subscription right after this call (eventual consistency).
418
+ */
419
+ public async setPlanSubscription(
420
+ planId: string,
421
+ paymentAccount: Identity,
422
+ opts?: ConsumeOptions,
423
+ ): Promise<TenantSubscription> {
424
+ return await this.sendCommand(
425
+ {
426
+ method: 'set',
427
+ uri: uri`/plans/${planId}/subscriptions`,
428
+ type: 'application/vnd.lime.identity',
429
+ resource: paymentAccount,
430
+ },
431
+ opts,
432
+ )
433
+ }
434
+
375
435
  public async deleteTenantUser(tenantId: string, user: Identity, opts?: ConsumeOptions): Promise<void> {
376
436
  return await this.sendCommand(
377
437
  {
@@ -1,4 +1,5 @@
1
1
  import type { BlipClient } from '../client.ts'
2
+ import type { Envelope } from '../types/envelope.ts'
2
3
  import type { Identity } from '../types/index.ts'
3
4
  import type { MessageTemplate, WhatsAppTemplateLanguage, WhatsappFlow } from '../types/whatsapp.ts'
4
5
  import { uri } from '../utils/uri.ts'
@@ -33,6 +34,65 @@ export class WhatsAppNamespace extends Namespace {
33
34
  }
34
35
  }
35
36
 
37
+ public getMessageMetadata(envelope: Envelope): {
38
+ /**
39
+ * Business-Scoped User ID of the user (e.g. `BR.11815799212886844830`).
40
+ * Unique per user ↔ business relationship: the same user has different BSUIDs across businesses.
41
+ */
42
+ bsuid?: string
43
+ /** Parent BSUID, correlating the user across eligible Business Accounts of the same organization */
44
+ parentId?: string
45
+ /** Username of the user, when adopted */
46
+ username?: string
47
+ } {
48
+ return {
49
+ bsuid: envelope.metadata?.['#wa.bsuid'] ?? undefined,
50
+ parentId: envelope.metadata?.['#wa.parentId'] ?? undefined,
51
+ username: envelope.metadata?.['#wa.username'] ?? undefined,
52
+ }
53
+ }
54
+
55
+ /**
56
+ * This does not query Meta in real time: numbers Blip hasn't seen a BSUID for are simply omitted from the result, which is not an error.
57
+ * Numbers must be in full form (country code + area code + number); the `+` sign is optional.
58
+ */
59
+ public async getContactsMapping(phoneNumbers: Array<string>, opts?: ConsumeOptions) {
60
+ const results: Array<{
61
+ /** Internal Blip contact id. Phone-based (e.g. `5531999999999`) or a Blip-internal GUID when the phone is not shared */
62
+ id: string
63
+ /** Business-Scoped User ID, unique per user ↔ business relationship (e.g. `BR.11815799212886844830`) */
64
+ bsuid: string
65
+ /** Phone number of the contact when available, without the `+` sign */
66
+ waId?: string
67
+ /** Correlation id between eligible Business Accounts of the same organization */
68
+ parentUserId?: string
69
+ /** Date of the most recent interaction of the contact registered by the platform */
70
+ lastReadDate?: string
71
+ }> = []
72
+
73
+ // The endpoint accepts at most 100 numbers per request and is throughput-limited,
74
+ // so chunks are sent sequentially instead of in parallel
75
+ for (let i = 0; i < phoneNumbers.length; i += 100) {
76
+ const chunk = phoneNumbers.slice(i, i + 100)
77
+ results.push(
78
+ ...(await this.sendCommand<'get', typeof results>(
79
+ {
80
+ method: 'get',
81
+ uri: uri`/external-contacts-mapping?${{ phoneNumbers: chunk.join(';') }}`,
82
+ },
83
+ {
84
+ ...opts,
85
+ collection: true,
86
+ // The full list is sent in a single request, there is no pagination
87
+ fetchall: false,
88
+ },
89
+ )),
90
+ )
91
+ }
92
+
93
+ return results
94
+ }
95
+
36
96
  /**
37
97
  * This is a heavily cached endpoint on Meta, so it may take a while to reflect changes
38
98
  * @param filters.status - Filter by status of the message template
@@ -27,7 +27,7 @@ export class CustomGatewayHttpSender extends ConnectionSender implements Sender
27
27
  'Content-Type': 'application/json',
28
28
  Authorization: `Basic ${this.token}`,
29
29
  },
30
- body: JSON.stringify(message),
30
+ body: this.serializeOutboundEnvelope(message),
31
31
  })
32
32
 
33
33
  if (!response.ok) {
@@ -52,7 +52,7 @@ export class HttpSender extends ConnectionSender implements Sender {
52
52
  public sendMessage<Type extends MessageTypes>(message: Message<Type>): Promise<void> {
53
53
  return this.withFetchRetryPolicy(async () => {
54
54
  await this.throttler.throttle('message')
55
- const response = await this.fetch('messages', JSON.stringify(message))
55
+ const response = await this.fetch('messages', this.serializeOutboundEnvelope(message))
56
56
 
57
57
  if (!response.ok) {
58
58
  throw new Error(`Failed to send message: ${response.statusText} (${response.status})`)
@@ -63,7 +63,7 @@ export class HttpSender extends ConnectionSender implements Sender {
63
63
  public sendCommand(command: Command<CommandMethods>): Promise<unknown> {
64
64
  return this.withFetchRetryPolicy(async () => {
65
65
  await this.throttler.throttle('command')
66
- const response = await this.fetch('commands', JSON.stringify(command))
66
+ const response = await this.fetch('commands', this.serializeOutboundEnvelope(command))
67
67
 
68
68
  if (!response.ok) {
69
69
  throw new Error(`Failed to send command: ${response.statusText} (${response.status})`)
@@ -2,6 +2,7 @@ import {
2
2
  type BlipDomain,
3
3
  type Command,
4
4
  type CommandMethods,
5
+ type Envelope,
5
6
  type Identity,
6
7
  type Message,
7
8
  type MessageTypes,
@@ -30,17 +31,30 @@ const ROOT_AUTHORITIES: ReadonlyArray<BlipDomain> = ['msging.net', 'blip.ai', '0
30
31
 
31
32
  export class ConnectionSender {
32
33
  private readonly _domain: BlipDomain
34
+ private readonly _node: Node
33
35
 
34
36
  constructor(options: ConstructorParameters<ConnectionSenderConstructor>[0]) {
35
- const nodeDomain = Node.from(options.node).domain ?? 'msging.net'
37
+ this._node = Node.from(options.node)
38
+ const nodeDomain = this._node.domain ?? 'msging.net'
36
39
  this._domain =
37
40
  ROOT_AUTHORITIES.find((root) => nodeDomain === root || nodeDomain.endsWith(`.${root}`)) ?? 'msging.net'
38
41
  }
39
42
 
43
+ public get node() {
44
+ return this._node
45
+ }
46
+
40
47
  public get domain() {
41
48
  return this._domain
42
49
  }
43
50
 
51
+ protected serializeOutboundEnvelope(envelope: Envelope): string {
52
+ if (envelope.from && !envelope.pp) {
53
+ envelope.pp = this.node
54
+ }
55
+ return JSON.stringify(envelope)
56
+ }
57
+
44
58
  // this method is a mess but provides a good and flexible api with a single method to login
45
59
  protected static login<S extends ConnectionSender>(bot: string | Identity, accessKey: string, tenantId?: string): S
46
60
  protected static login<S extends ConnectionSender>(token: string, tenantId?: string): S
@@ -146,6 +160,11 @@ export abstract class OpenConnectionSender extends ConnectionSender implements S
146
160
  return this.sessionNegotiator?.session ?? null
147
161
  }
148
162
 
163
+ public override get node() {
164
+ // the negotiated local node is authoritative: it carries the server-assigned instance
165
+ return this.sessionNegotiator?.session?.localNode ?? super.node
166
+ }
167
+
149
168
  public close(): Promise<void> {
150
169
  this.envelopeResolver.close()
151
170
  return Promise.resolve()
@@ -74,7 +74,7 @@ export class TCPSender extends OpenConnectionSender {
74
74
  const socket = await this.connectionHandle.get()
75
75
  await this.sessionNegotiator?.ensurePresence()
76
76
 
77
- socket.write(JSON.stringify(message))
77
+ socket.write(this.serializeOutboundEnvelope(message))
78
78
  }
79
79
 
80
80
  public sendCommand(command: Command<CommandMethods>): Promise<unknown> {
@@ -86,7 +86,7 @@ export class TCPSender extends OpenConnectionSender {
86
86
  const socket = await this.connectionHandle.get()
87
87
  await this.sessionNegotiator?.ensurePresence(command.uri)
88
88
 
89
- socket.write(JSON.stringify(command))
89
+ socket.write(this.serializeOutboundEnvelope(command))
90
90
 
91
91
  const response = (await envelopeResponsePromise) as CommandResponse<
92
92
  unknown,
@@ -56,7 +56,7 @@ export class WebSocketSender extends OpenConnectionSender {
56
56
  const webSocket = await this.connectionHandle.get()
57
57
  await this.sessionNegotiator?.ensurePresence()
58
58
 
59
- webSocket.send(JSON.stringify(message))
59
+ webSocket.send(this.serializeOutboundEnvelope(message))
60
60
  }
61
61
 
62
62
  public sendCommand(command: Command<CommandMethods>): Promise<unknown> {
@@ -67,7 +67,7 @@ export class WebSocketSender extends OpenConnectionSender {
67
67
 
68
68
  const webSocket = await this.connectionHandle.get()
69
69
  await this.sessionNegotiator?.ensurePresence(command.uri)
70
- webSocket.send(JSON.stringify(command))
70
+ webSocket.send(this.serializeOutboundEnvelope(command))
71
71
 
72
72
  const response = (await envelopeResponsePromise) as CommandResponse<
73
73
  unknown,
@@ -1,9 +1,10 @@
1
1
  export type Plan = {
2
+ id: string
2
3
  name: string
3
4
  ownerIdentity: string
4
- planValue: number
5
- currencyCode: string
6
- extras: {
5
+ planValue?: number
6
+ currencyCode?: string
7
+ extras: Partial<{
7
8
  Cluster: string
8
9
  CommandThroughput: string
9
10
  DataRetentionPeriod: string
@@ -11,5 +12,5 @@ export type Plan = {
11
12
  IsDefaultPlan: string
12
13
  MessageThroughput: string
13
14
  NotificationThroughput: string
14
- }
15
+ }>
15
16
  }