ab-ecosystem-cpass 1.0.0 → 1.0.6
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/README.md +121 -21
- package/dist/client.d.ts +38 -0
- package/dist/client.js +49 -5
- package/dist/client.js.map +1 -1
- package/dist/http.d.ts +15 -0
- package/dist/http.js +21 -1
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.js +24 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/bytes.d.ts +38 -0
- package/dist/internal/bytes.js +84 -0
- package/dist/internal/bytes.js.map +1 -0
- package/dist/internal/device.d.ts +24 -0
- package/dist/internal/device.js +91 -0
- package/dist/internal/device.js.map +1 -0
- package/dist/internal/md5.d.ts +4 -0
- package/dist/internal/md5.js +109 -0
- package/dist/internal/md5.js.map +1 -0
- package/dist/internal/receiver-file.d.ts +106 -0
- package/dist/internal/receiver-file.js +128 -0
- package/dist/internal/receiver-file.js.map +1 -0
- package/dist/realtime/client.d.ts +85 -0
- package/dist/realtime/client.js +126 -0
- package/dist/realtime/client.js.map +1 -0
- package/dist/realtime/events.d.ts +147 -0
- package/dist/realtime/events.js +3 -0
- package/dist/realtime/events.js.map +1 -0
- package/dist/realtime/inbox.d.ts +173 -0
- package/dist/realtime/inbox.js +263 -0
- package/dist/realtime/inbox.js.map +1 -0
- package/dist/realtime/index.d.ts +16 -0
- package/dist/realtime/index.js +37 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/resources/audience/contacts.d.ts +19 -3
- package/dist/resources/audience/contacts.js +19 -3
- package/dist/resources/audience/contacts.js.map +1 -1
- package/dist/resources/audience/inbox-media.d.ts +92 -0
- package/dist/resources/audience/inbox-media.js +127 -0
- package/dist/resources/audience/inbox-media.js.map +1 -0
- package/dist/resources/audience/notifications.d.ts +171 -0
- package/dist/resources/audience/notifications.js +215 -0
- package/dist/resources/audience/notifications.js.map +1 -0
- package/dist/resources/audience/scheduled-messages.d.ts +234 -0
- package/dist/resources/audience/scheduled-messages.js +259 -0
- package/dist/resources/audience/scheduled-messages.js.map +1 -0
- package/dist/resources/campaign-audience.d.ts +132 -0
- package/dist/resources/campaign-audience.js +224 -0
- package/dist/resources/campaign-audience.js.map +1 -0
- package/dist/resources/campaign-report-exports.d.ts +14 -0
- package/dist/resources/campaign-report-exports.js +28 -0
- package/dist/resources/campaign-report-exports.js.map +1 -0
- package/dist/resources/campaign-reports.d.ts +15 -0
- package/dist/resources/campaign-reports.js +25 -0
- package/dist/resources/campaign-reports.js.map +1 -0
- package/dist/resources/campaigns.d.ts +239 -0
- package/dist/resources/campaigns.js +300 -0
- package/dist/resources/campaigns.js.map +1 -0
- package/dist/types/audience.d.ts +93 -1
- package/dist/types/campaign.d.ts +229 -0
- package/dist/types/campaign.js +3 -0
- package/dist/types/campaign.js.map +1 -0
- package/dist/types/contact-filter.d.ts +3 -2
- package/dist/types/contact-filter.js +3 -2
- package/dist/types/contact-filter.js.map +1 -1
- package/dist/types/inbox.d.ts +391 -0
- package/dist/types/inbox.js +16 -0
- package/dist/types/inbox.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/scheduled-message.d.ts +221 -0
- package/dist/types/scheduled-message.js +16 -0
- package/dist/types/scheduled-message.js.map +1 -0
- package/docs/audience/contacts.md +34 -4
- package/docs/audience/notifications.md +263 -0
- package/docs/audience/scheduled-messages.md +557 -0
- package/docs/campaign/campaigns.md +489 -0
- package/docs/contact-filter.md +2 -1
- package/docs/inbox/chats.md +356 -0
- package/docs/inbox/connection.md +171 -0
- package/docs/inbox/media.md +202 -0
- package/docs/inbox/messages.md +338 -0
- package/docs/inbox/referrals.md +72 -0
- package/docs/inbox/sessions.md +74 -0
- package/package.json +28 -2
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# Inbox — chats
|
|
2
|
+
|
|
3
|
+
A chat *is* a contact. There is no separate conversation record: the inbox state
|
|
4
|
+
lives on the contact document, which is why archiving a chat and editing a
|
|
5
|
+
contact through the REST API touch the same row and why both show up in each
|
|
6
|
+
other's views.
|
|
7
|
+
|
|
8
|
+
`inbox.chats.*` · see [connection.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/connection.md) first
|
|
9
|
+
|
|
10
|
+
| Action | Event | Scope |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `list` | `contact.list` | `contacts:read` |
|
|
13
|
+
| `get` | `contact.fetch` | `contacts:read` |
|
|
14
|
+
| `delete` `transfer` `bulkTransfer` `archive` `unarchive` `block` `markAsRead` `markAsUnread` `pauseAutomation` `resumeAutomation` `close` `done` `pauseTimer` `updateTags` | `chat.action` | `contacts:write` |
|
|
15
|
+
| `update` | `contact.update` | `contacts:write` |
|
|
16
|
+
| `assignBot` | `contact.bot_assign` | `contacts:write` |
|
|
17
|
+
|
|
18
|
+
## The chat shape
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
interface Chat {
|
|
22
|
+
id: string;
|
|
23
|
+
name: { salutation: string; first_name: string; last_name: string };
|
|
24
|
+
/** One entry per channel the contact is reachable on, keyed by channel kind. */
|
|
25
|
+
profile: Record<string, ContactProfile>;
|
|
26
|
+
chat_metadata: {
|
|
27
|
+
new: boolean; // nobody has answered yet
|
|
28
|
+
archived: boolean;
|
|
29
|
+
unread: number; // account-wide, not per agent
|
|
30
|
+
pause_duration: number; // seconds automation stays paused; default 3600
|
|
31
|
+
marked_as_unread: boolean; // flagged by hand
|
|
32
|
+
closed: boolean;
|
|
33
|
+
marked_as_done: boolean;
|
|
34
|
+
blocked: { incoming: boolean; outgoing: boolean; bot: boolean; api: boolean };
|
|
35
|
+
};
|
|
36
|
+
last_seen: number; // unix seconds of the last inbound message
|
|
37
|
+
bot: JsonObject; // flow state: id, node, attempt, pause, pause_timestamp
|
|
38
|
+
last_interaction_props: JsonObject; // last-message preview
|
|
39
|
+
agent: string | null;
|
|
40
|
+
team: string | null;
|
|
41
|
+
tags: ContactTag[];
|
|
42
|
+
custom_field_hash?: JsonObject; // list rows only
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`unread` is one counter on the chat, not one per agent. In a shared inbox, one
|
|
47
|
+
agent opening a thread clears it for everyone.
|
|
48
|
+
|
|
49
|
+
`last_interaction_props` is a denormalised preview of the most recent message, so
|
|
50
|
+
rendering a list of chats needs no read of the message history at all.
|
|
51
|
+
|
|
52
|
+
## list
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const { contacts, filter, nextPage } = await inbox.chats.list({
|
|
56
|
+
filter: { assignment: 'me', unread: 'unread', closed: 'open' },
|
|
57
|
+
limit: 20,
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`contact.list` · scope `contacts:read`
|
|
62
|
+
|
|
63
|
+
| Field | Type | Required | Default | Notes |
|
|
64
|
+
|---|---|---|---|---|
|
|
65
|
+
| `filter` | `ChatListFilter` | no | `{}` | The view. Every key independent; see below. |
|
|
66
|
+
| `conditions` | `ContactFilterEntry[]` | no | `[]` | Up to 50. The same DSL the contact list and segments use. |
|
|
67
|
+
| `search` | `string` | no | — | Max 120 chars. Matches first name, last name and every channel identity. |
|
|
68
|
+
| `nextPage` | `number` | no | `0` | Cursor: the `last_seen` of the oldest row you hold. |
|
|
69
|
+
| `limit` | `number` | no | `20` | Max 100. |
|
|
70
|
+
| `department` | `string` | no | — | Narrow to one team. |
|
|
71
|
+
| `team_member` | `string` | no | — | Narrow to one member's chats. Intersects whichever view is active. |
|
|
72
|
+
|
|
73
|
+
### The filter
|
|
74
|
+
|
|
75
|
+
Each key is independent, and omitting one means "don't care". An empty filter is
|
|
76
|
+
every live chat. Trashed chats never appear, with or without a search term.
|
|
77
|
+
|
|
78
|
+
| Key | Values | Matches |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `assignment` | `all` | Every chat, assigned to anyone or no one. |
|
|
81
|
+
| | `me` | Chats assigned to the calling member. |
|
|
82
|
+
| | `none` | The unassigned queue. |
|
|
83
|
+
| `unread` | `unread` | Counter above zero **or** flagged unread by hand. |
|
|
84
|
+
| | `replied` | Caught up: counter at zero and no hand flag. |
|
|
85
|
+
| `new` | `new` | Nobody has answered yet. |
|
|
86
|
+
| | `old` | Everything else. |
|
|
87
|
+
| `closed` | `closed` | Closed **or** marked done. |
|
|
88
|
+
| | `open` | Neither. |
|
|
89
|
+
| `archived` | `archived` / `unarchived` | The archive flag. |
|
|
90
|
+
| `automation` | `active` | Automation running. |
|
|
91
|
+
| | `paused` | Automation held. |
|
|
92
|
+
| | `active_no_bot` | Running, with no flow attached. |
|
|
93
|
+
| `block` | `ic` / `og` / `api` / `bot` | That one block flag is set. |
|
|
94
|
+
| | `unblocked` | None of the four set. |
|
|
95
|
+
| `session` | `active` | Seen inside the last 24 hours — the provider reply window. |
|
|
96
|
+
| | `expired` | Outside it, or never seen. |
|
|
97
|
+
|
|
98
|
+
### Paging
|
|
99
|
+
|
|
100
|
+
Keyset, not offset. Rows are ordered by `last_seen` descending; `nextPage` is the
|
|
101
|
+
`last_seen` of the last row returned, and `null` once the list is exhausted.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
let cursor: number | undefined;
|
|
105
|
+
const all: Chat[] = [];
|
|
106
|
+
|
|
107
|
+
do {
|
|
108
|
+
const page = await inbox.chats.list({ nextPage: cursor, limit: 50 });
|
|
109
|
+
all.push(...page.contacts);
|
|
110
|
+
cursor = page.nextPage ?? undefined;
|
|
111
|
+
} while (cursor);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This holds a moving list stable in a way offset paging cannot: a chat that gets a
|
|
115
|
+
new message while you are paging jumps to the top, and offsets would then shift
|
|
116
|
+
every row after it.
|
|
117
|
+
|
|
118
|
+
### The applied filter
|
|
119
|
+
|
|
120
|
+
The reply's `filter` is the filter that was **applied**, which is not always the
|
|
121
|
+
one you sent. A team member without `live_chat.unassigned.ALL` asking for `all`
|
|
122
|
+
is narrowed to `me`:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const page = await inbox.chats.list({ filter: { assignment: 'all' } });
|
|
126
|
+
page.filter.assignment; // 'me' for a restricted team member
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Render what came back, not what you asked for — asking for `all` and silently
|
|
130
|
+
showing a shorter list looks like missing data.
|
|
131
|
+
|
|
132
|
+
### conditions
|
|
133
|
+
|
|
134
|
+
The same condition DSL as the contact list and saved segments. `and` binds
|
|
135
|
+
tighter than `or`; the list is cut at every `or`. See
|
|
136
|
+
[contact-filter.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/contact-filter.md).
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
await inbox.chats.list({
|
|
140
|
+
conditions: [
|
|
141
|
+
{ field_id: 'city', field_type: 'string', condition: 'equal to', value: 'Pune', operator: null },
|
|
142
|
+
{ field_id: 'REFERRAL_ID', field_type: 'string', condition: 'equal to', value: '120210000000', operator: 'and' },
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`REFERRAL_ID` is available here and **not** on the REST contact list: it resolves
|
|
148
|
+
through the click-to-WhatsApp referrals, which only the inbox reads. See
|
|
149
|
+
[referrals.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/referrals.md).
|
|
150
|
+
|
|
151
|
+
## get
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const chat = await inbox.chats.get(chatId);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`contact.fetch` · scope `contacts:read` · `404` if no such chat for this account
|
|
158
|
+
|
|
159
|
+
Also **subscribes to the thread**: messages and state changes for this
|
|
160
|
+
conversation reach the socket from here on without asking again.
|
|
161
|
+
|
|
162
|
+
## Actions
|
|
163
|
+
|
|
164
|
+
Every action answers the caller and broadcasts the same thing to the account — an
|
|
165
|
+
inbox is shared, so one agent archiving a chat has to make it leave everyone
|
|
166
|
+
else's list too. Both arrive as `chat.action`:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
interface ChatActionResult {
|
|
170
|
+
action: ChatActionName;
|
|
171
|
+
id: string;
|
|
172
|
+
chat?: Chat | null; // null for delete: the row has left the list
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Every action takes the chat id as `chat`. `id` is accepted as an alias on all of
|
|
177
|
+
them, because the original clients use one or the other depending on the action.
|
|
178
|
+
|
|
179
|
+
### delete
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
await inbox.chats.delete(chatId);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Trashes the contact. The chat leaves every inbox view; the row and its history
|
|
186
|
+
stay. `chat` comes back `null` — there is nothing to draw.
|
|
187
|
+
|
|
188
|
+
### transfer
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
await inbox.chats.transfer(chatId, memberId);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
| Field | Type | Required | Notes |
|
|
195
|
+
|---|---|---|---|
|
|
196
|
+
| `chat` | `string` | yes | |
|
|
197
|
+
| `member` | `string` | yes | Team member to assign to. |
|
|
198
|
+
|
|
199
|
+
Also flags the chat unread for the receiving member, so it surfaces in their
|
|
200
|
+
`unread` view.
|
|
201
|
+
|
|
202
|
+
### bulkTransfer
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const { count } = await inbox.chats.bulkTransfer({ source: 'UNASSIGN', target: memberId });
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
| Field | Type | Required | Default | Notes |
|
|
209
|
+
|---|---|---|---|---|
|
|
210
|
+
| `source` | `string \| 'UNASSIGN'` | yes | — | Member to take chats from, or the unassigned queue. |
|
|
211
|
+
| `target` | `string \| 'UNASSIGN'` | yes | — | Member or team to hand them to, or clear the assignment. |
|
|
212
|
+
| `target_type` | `'member' \| 'department'` | no | `'member'` | What `target` names. |
|
|
213
|
+
|
|
214
|
+
Capped at **500 chats per call**. Each moved chat is broadcast individually under
|
|
215
|
+
`action: 'transfer'`, so clients need no batch shape. `target: 'UNASSIGN'` clears
|
|
216
|
+
the team as well as the agent.
|
|
217
|
+
|
|
218
|
+
### archive / unarchive
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
await inbox.chats.archive(chatId);
|
|
222
|
+
await inbox.chats.unarchive(chatId);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### block
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
await inbox.chats.block(chatId, { incoming: true, bot: true });
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
| Field | Type | Required | Notes |
|
|
232
|
+
|---|---|---|---|
|
|
233
|
+
| `update.incoming` | `boolean` | no | Drop inbound messages. |
|
|
234
|
+
| `update.outgoing` | `boolean` | no | Refuse outbound sends. |
|
|
235
|
+
| `update.bot` | `boolean` | no | Keep automation off this chat. |
|
|
236
|
+
| `update.api` | `boolean` | no | Refuse API sends. |
|
|
237
|
+
|
|
238
|
+
**Only the flags you send change.** Flags left out keep their current value.
|
|
239
|
+
|
|
240
|
+
### markAsRead / markAsUnread
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
await inbox.chats.markAsRead(chatId);
|
|
244
|
+
await inbox.chats.markAsUnread(chatId);
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`markAsRead` clears both the counter and the hand flag. `markAsUnread` sets the
|
|
248
|
+
hand flag only, leaving the counter alone.
|
|
249
|
+
|
|
250
|
+
### pauseAutomation / resumeAutomation
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
await inbox.chats.pauseAutomation(chatId);
|
|
254
|
+
await inbox.chats.resumeAutomation(chatId);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`pauseAutomation` detaches the flow entirely — id, node, attempt counter and
|
|
258
|
+
waiting state — and holds automation for `chat_metadata.pause_duration` seconds
|
|
259
|
+
(default 3600). The pause is stamped with a token, so re-pausing a chat
|
|
260
|
+
invalidates the earlier resume rather than letting it fire early.
|
|
261
|
+
|
|
262
|
+
Sending a message by hand pauses automation on its own; see
|
|
263
|
+
[messages.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/messages.md#send).
|
|
264
|
+
|
|
265
|
+
### pauseTimer
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
await inbox.chats.pauseTimer(chatId, 7200);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
| Field | Type | Required | Notes |
|
|
272
|
+
|---|---|---|---|
|
|
273
|
+
| `duration` | `number` | yes | Seconds, `0`–`2592000` (30 days). |
|
|
274
|
+
|
|
275
|
+
Sets a new `pause_duration` and re-stamps the pause from now.
|
|
276
|
+
|
|
277
|
+
### close / done
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
await inbox.chats.close(chatId);
|
|
281
|
+
await inbox.chats.done(chatId, true);
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Both detach the flow. They differ in what else they touch:
|
|
285
|
+
|
|
286
|
+
| | `close` | `done` |
|
|
287
|
+
|---|---|---|
|
|
288
|
+
| `chat_metadata.closed` | `true` | unchanged |
|
|
289
|
+
| `chat_metadata.marked_as_done` | unchanged | `true` |
|
|
290
|
+
| `chat_metadata.new` | unchanged | `false` |
|
|
291
|
+
| `agent` | unchanged | `null` |
|
|
292
|
+
|
|
293
|
+
So `close` ends the conversation but keeps the agent on it; `done` hands it back.
|
|
294
|
+
`done` takes an optional `isEnded` flag, which travels to assignment analytics.
|
|
295
|
+
|
|
296
|
+
The `closed` filter matches either.
|
|
297
|
+
|
|
298
|
+
### updateTags
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
await inbox.chats.updateTags(chatId, [tagId1, tagId2]);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
| Field | Type | Required | Notes |
|
|
305
|
+
|---|---|---|---|
|
|
306
|
+
| `tags` | `string[]` | yes | Up to 100 tag ids. **Replaces** the set outright. |
|
|
307
|
+
|
|
308
|
+
Send the full list, not a delta. Passing `[]` clears every tag.
|
|
309
|
+
|
|
310
|
+
## update
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
const { chat_metadata } = await inbox.chats.update({ id: chatId, action: 'done' });
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`contact.update` · scope `contacts:write`
|
|
317
|
+
|
|
318
|
+
| Field | Type | Required | Notes |
|
|
319
|
+
|---|---|---|---|
|
|
320
|
+
| `id` | `string` | yes | |
|
|
321
|
+
| `action` | `'done' \| 'archive'` | yes | |
|
|
322
|
+
|
|
323
|
+
A narrower action set whose reply carries only the metadata. It does the same
|
|
324
|
+
thing as `chats.done` / `chats.archive` and exists because the original clients
|
|
325
|
+
hold this contract — prefer the action methods in new code.
|
|
326
|
+
|
|
327
|
+
## assignBot
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
await inbox.chats.assignBot({ chat: chatId, channel: channelId, bot: botId, node: 'welcome' });
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
`contact.bot_assign` · scope `contacts:write`
|
|
334
|
+
|
|
335
|
+
| Field | Type | Required | Default | Notes |
|
|
336
|
+
|---|---|---|---|---|
|
|
337
|
+
| `chat` | `string` | yes | — | |
|
|
338
|
+
| `channel` | `string` | yes | — | Channel the flow runs on. |
|
|
339
|
+
| `bot` | `string` | yes | — | Flow id. |
|
|
340
|
+
| `node` | `string` | no | `''` | Where in the flow to start. |
|
|
341
|
+
| `action` | `'assign' \| 'trigger'` | no | `'assign'` | |
|
|
342
|
+
|
|
343
|
+
`assign` writes the flow pointer directly and resumes automation, which arrives
|
|
344
|
+
as a `chat.action` with `action: 'resume_automation'`.
|
|
345
|
+
|
|
346
|
+
`trigger` publishes instead and answers immediately: where in a flow a chat
|
|
347
|
+
belongs is the bot server's decision, so the chat's new state arrives as a push
|
|
348
|
+
once it has decided.
|
|
349
|
+
|
|
350
|
+
## Service-side notes
|
|
351
|
+
|
|
352
|
+
Not SDK behaviour, but worth knowing:
|
|
353
|
+
|
|
354
|
+
- **Deleting a contact tag does not pull it off the chats carrying it.** A tag id
|
|
355
|
+
can outlive its tag; filter defensively.
|
|
356
|
+
- **`unread` is account-wide.** There is no per-agent read state.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Inbox — connecting
|
|
2
|
+
|
|
3
|
+
The live-chat inbox is a Socket.IO connection, not REST. It has its own origin,
|
|
4
|
+
separate from the REST gateway: the gateway proxies HTTP only and forwards no
|
|
5
|
+
WebSocket upgrade, so a handshake routed through it never completes.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install ab-ecosystem-cpass socket.io-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`socket.io-client` is an optional peer dependency. The REST client never resolves
|
|
12
|
+
it — only the `/realtime` subpath does.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { Inbox } from 'ab-ecosystem-cpass/realtime';
|
|
16
|
+
|
|
17
|
+
const inbox = new Inbox({
|
|
18
|
+
url: 'https://inbox.example.com',
|
|
19
|
+
clientId: process.env.CRM_CLIENT_ID!,
|
|
20
|
+
token: () => session.accessToken, // called on every connect
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await inbox.ready();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Client options
|
|
27
|
+
|
|
28
|
+
| Option | Type | Default | Notes |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `url` | `string` | — | Required. The inbox socket's origin. |
|
|
31
|
+
| `clientId` | `string` | — | Required. Your ecosystem client id, as on every REST call. |
|
|
32
|
+
| `token` | `string \| () => string \| Promise<string>` | — | Required. Bearer token. |
|
|
33
|
+
| `path` | `string` | `/socket.io` | Only if the deployment moved it. |
|
|
34
|
+
| `timeout` | `number` | `20000` | How long a request waits for its acknowledgement, in ms. |
|
|
35
|
+
| `autoConnect` | `boolean` | `true` | Connect on construction. |
|
|
36
|
+
| `socketOptions` | `object` | — | Merged into the socket.io options last. |
|
|
37
|
+
|
|
38
|
+
**Pass `token` as a function.** It is resolved on every connect, so a reconnect
|
|
39
|
+
after an expiry picks up a fresh token. A string is captured once, and a socket
|
|
40
|
+
that drops after the token expires will retry forever with the dead one.
|
|
41
|
+
|
|
42
|
+
## How credentials travel
|
|
43
|
+
|
|
44
|
+
In `auth`, not in a header:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
{ token: '<bearer token>', client_id: '<client id>' }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A browser cannot set headers on a WebSocket upgrade at all. A header-based
|
|
51
|
+
handshake only works while the connection is still on HTTP long-polling, and
|
|
52
|
+
breaks the moment a client sets `transports: ['websocket']`. The server still
|
|
53
|
+
accepts `authorization` and `client-id` headers for existing clients.
|
|
54
|
+
|
|
55
|
+
The handshake checks two things: the token verifies against the ecosystem JWKS
|
|
56
|
+
for the `client_id` sent, and the verified claims name that same client. There is
|
|
57
|
+
**no origin allowlist** — the credential is the boundary, not the page it came
|
|
58
|
+
from, which is also why a server-to-server client with no `Origin` at all
|
|
59
|
+
connects fine. A failure arrives as `connect_error` with the reason:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
inbox.raw.on('connect_error', (err) => console.error(err.message));
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Requests and pushes
|
|
66
|
+
|
|
67
|
+
Two kinds of traffic, and the same event name can be both.
|
|
68
|
+
|
|
69
|
+
**Requests** are promises. Every emit carries an acknowledgement callback, so two
|
|
70
|
+
concurrent reads of different threads cannot be confused for one another:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const { contacts, nextPage } = await inbox.chats.list({ filter: { assignment: 'me' } });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Pushes** are things that happened elsewhere — another agent archiving a chat, a
|
|
77
|
+
contact replying:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const off = inbox.on('chat.action', ({ action, id, chat }) => applyToList(action, id, chat));
|
|
81
|
+
off(); // unsubscribe
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
An archive you performed and an archive another agent performed arrive under the
|
|
85
|
+
same name with the same shape, so one handler per kind of change is enough.
|
|
86
|
+
|
|
87
|
+
| Push | Payload | When |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `message.received` | `Message` | A message arrived, or one you sent now exists as a stored row. |
|
|
90
|
+
| `message.status` | `MessageStatusUpdate` | A delivery state moved: sent, delivered, read, failed. |
|
|
91
|
+
| `chat.action` | `ChatActionResult` | A chat changed: unread, assignment, automation, tags. |
|
|
92
|
+
| `contact.received` | stored document | A contact wrote in for the first time, so a chat row appeared. |
|
|
93
|
+
| `message.delete` | `MessageDeleteResult` | A message was removed from an open thread. |
|
|
94
|
+
| `message.retry_download` | `Message` | Media was re-fetched and its link changed. |
|
|
95
|
+
| `contact.done` | `ChatActionResult` | A chat was marked done. |
|
|
96
|
+
| `message.send` | stored document | Legacy. The same row as `message.received`, unmapped. |
|
|
97
|
+
| `error` | `InboxError` | An event was refused. Carries the failing event name. |
|
|
98
|
+
|
|
99
|
+
`message.send` as a *push* exists only because the inbox this replaced used one
|
|
100
|
+
name for both its own reply and the row that followed. Prefer
|
|
101
|
+
`message.received`: it is mapped (`id`, ISO `createdAt`), where `message.send`
|
|
102
|
+
carries `_id` and `user` as the database holds them.
|
|
103
|
+
|
|
104
|
+
Each push is delivered **once** per socket even when you are in two rooms it
|
|
105
|
+
targets — your account's room and the thread you have open.
|
|
106
|
+
|
|
107
|
+
## Subscriptions
|
|
108
|
+
|
|
109
|
+
A socket starts subscribed to the account, so every account-wide change reaches
|
|
110
|
+
it. Thread-level pushes need the thread: calling `chats.get(id)` or
|
|
111
|
+
`messages.list(id)` subscribes to that conversation, and typing notices and new
|
|
112
|
+
messages for it arrive from then on without asking again.
|
|
113
|
+
|
|
114
|
+
## Errors
|
|
115
|
+
|
|
116
|
+
A refused request rejects with `CrmApiError`, carrying the same `status` and
|
|
117
|
+
`code` the REST API would have returned — one error path covers both transports.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { CrmApiError } from 'ab-ecosystem-cpass';
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await inbox.chats.transfer(chatId, memberId);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (err instanceof CrmApiError && err.status === 403) {
|
|
126
|
+
// not allowed to reassign this chat
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
| `status` | `code` | Means |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| 401 | `unauthorized` | The handshake's token did not verify. |
|
|
134
|
+
| 403 | `forbidden` | The action is beyond this member's permissions. |
|
|
135
|
+
| 404 | `not_found` | No such chat or message **for this account**. |
|
|
136
|
+
| 422 | `validation_failed` | Payload rejected. `details` names the fields. |
|
|
137
|
+
| 500 | `internal_error` | Server fault. Nothing further is disclosed. |
|
|
138
|
+
| 504 | `timeout` | No acknowledgement inside `timeout` ms. |
|
|
139
|
+
|
|
140
|
+
An unknown event name is ignored rather than refused, so a client emitting its
|
|
141
|
+
own lifecycle events gets no spurious errors.
|
|
142
|
+
|
|
143
|
+
## Scoping
|
|
144
|
+
|
|
145
|
+
Every read and write is scoped to the account the token identifies, the same way
|
|
146
|
+
the REST contact endpoints are. Quoting another account's chat or message id
|
|
147
|
+
produces `404`, not that account's data.
|
|
148
|
+
|
|
149
|
+
## Permissions
|
|
150
|
+
|
|
151
|
+
Visibility inside an account is gated on inbox permissions, not API scopes. An
|
|
152
|
+
owner or a manager is never gated; a team member needs the permission named.
|
|
153
|
+
|
|
154
|
+
| Permission | Grants |
|
|
155
|
+
|---|---|
|
|
156
|
+
| `live_chat.unassigned.ALL` | See every chat in the account, assigned or not. |
|
|
157
|
+
| `live_chat.unassigned.NEW` | See chats nobody has answered yet. |
|
|
158
|
+
| `live_chat.send.AS_OTHER` | Attribute an outgoing message to another member. |
|
|
159
|
+
|
|
160
|
+
A team member without these is narrowed to their own chats rather than refused —
|
|
161
|
+
and the reply says so. See [chats.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/chats.md#the-applied-filter).
|
|
162
|
+
|
|
163
|
+
## Modules
|
|
164
|
+
|
|
165
|
+
| Page | Actions |
|
|
166
|
+
|---|---|
|
|
167
|
+
| [chats.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/chats.md) | `list` `get` `delete` `transfer` `bulkTransfer` `archive` `unarchive` `block` `markAsRead` `markAsUnread` `pauseAutomation` `resumeAutomation` `close` `done` `pauseTimer` `updateTags` `update` `assignBot` |
|
|
168
|
+
| [messages.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/messages.md) | `list` `get` `send` `sendMedia` `delete` `retryDownload` |
|
|
169
|
+
| [media.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/media.md) | `sendMedia` · `crm.inboxMedia.upload` `createUpload` |
|
|
170
|
+
| [referrals.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/referrals.md) | `referrals` |
|
|
171
|
+
| [sessions.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/sessions.md) | `sessions` |
|