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.
Files changed (86) hide show
  1. package/README.md +121 -21
  2. package/dist/client.d.ts +38 -0
  3. package/dist/client.js +49 -5
  4. package/dist/client.js.map +1 -1
  5. package/dist/http.d.ts +15 -0
  6. package/dist/http.js +21 -1
  7. package/dist/http.js.map +1 -1
  8. package/dist/index.d.ts +11 -0
  9. package/dist/index.js +24 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/internal/bytes.d.ts +38 -0
  12. package/dist/internal/bytes.js +84 -0
  13. package/dist/internal/bytes.js.map +1 -0
  14. package/dist/internal/device.d.ts +24 -0
  15. package/dist/internal/device.js +91 -0
  16. package/dist/internal/device.js.map +1 -0
  17. package/dist/internal/md5.d.ts +4 -0
  18. package/dist/internal/md5.js +109 -0
  19. package/dist/internal/md5.js.map +1 -0
  20. package/dist/internal/receiver-file.d.ts +106 -0
  21. package/dist/internal/receiver-file.js +128 -0
  22. package/dist/internal/receiver-file.js.map +1 -0
  23. package/dist/realtime/client.d.ts +85 -0
  24. package/dist/realtime/client.js +126 -0
  25. package/dist/realtime/client.js.map +1 -0
  26. package/dist/realtime/events.d.ts +147 -0
  27. package/dist/realtime/events.js +3 -0
  28. package/dist/realtime/events.js.map +1 -0
  29. package/dist/realtime/inbox.d.ts +173 -0
  30. package/dist/realtime/inbox.js +263 -0
  31. package/dist/realtime/inbox.js.map +1 -0
  32. package/dist/realtime/index.d.ts +16 -0
  33. package/dist/realtime/index.js +37 -0
  34. package/dist/realtime/index.js.map +1 -0
  35. package/dist/resources/audience/contacts.d.ts +19 -3
  36. package/dist/resources/audience/contacts.js +19 -3
  37. package/dist/resources/audience/contacts.js.map +1 -1
  38. package/dist/resources/audience/inbox-media.d.ts +92 -0
  39. package/dist/resources/audience/inbox-media.js +127 -0
  40. package/dist/resources/audience/inbox-media.js.map +1 -0
  41. package/dist/resources/audience/notifications.d.ts +171 -0
  42. package/dist/resources/audience/notifications.js +215 -0
  43. package/dist/resources/audience/notifications.js.map +1 -0
  44. package/dist/resources/audience/scheduled-messages.d.ts +234 -0
  45. package/dist/resources/audience/scheduled-messages.js +259 -0
  46. package/dist/resources/audience/scheduled-messages.js.map +1 -0
  47. package/dist/resources/campaign-audience.d.ts +132 -0
  48. package/dist/resources/campaign-audience.js +224 -0
  49. package/dist/resources/campaign-audience.js.map +1 -0
  50. package/dist/resources/campaign-report-exports.d.ts +14 -0
  51. package/dist/resources/campaign-report-exports.js +28 -0
  52. package/dist/resources/campaign-report-exports.js.map +1 -0
  53. package/dist/resources/campaign-reports.d.ts +15 -0
  54. package/dist/resources/campaign-reports.js +25 -0
  55. package/dist/resources/campaign-reports.js.map +1 -0
  56. package/dist/resources/campaigns.d.ts +239 -0
  57. package/dist/resources/campaigns.js +300 -0
  58. package/dist/resources/campaigns.js.map +1 -0
  59. package/dist/types/audience.d.ts +93 -1
  60. package/dist/types/campaign.d.ts +229 -0
  61. package/dist/types/campaign.js +3 -0
  62. package/dist/types/campaign.js.map +1 -0
  63. package/dist/types/contact-filter.d.ts +3 -2
  64. package/dist/types/contact-filter.js +3 -2
  65. package/dist/types/contact-filter.js.map +1 -1
  66. package/dist/types/inbox.d.ts +391 -0
  67. package/dist/types/inbox.js +16 -0
  68. package/dist/types/inbox.js.map +1 -0
  69. package/dist/types/index.d.ts +3 -0
  70. package/dist/types/index.js +5 -0
  71. package/dist/types/index.js.map +1 -1
  72. package/dist/types/scheduled-message.d.ts +221 -0
  73. package/dist/types/scheduled-message.js +16 -0
  74. package/dist/types/scheduled-message.js.map +1 -0
  75. package/docs/audience/contacts.md +34 -4
  76. package/docs/audience/notifications.md +263 -0
  77. package/docs/audience/scheduled-messages.md +557 -0
  78. package/docs/campaign/campaigns.md +489 -0
  79. package/docs/contact-filter.md +2 -1
  80. package/docs/inbox/chats.md +356 -0
  81. package/docs/inbox/connection.md +171 -0
  82. package/docs/inbox/media.md +202 -0
  83. package/docs/inbox/messages.md +338 -0
  84. package/docs/inbox/referrals.md +72 -0
  85. package/docs/inbox/sessions.md +74 -0
  86. package/package.json +28 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"campaign-report-exports.js","sourceRoot":"","sources":["../../src/resources/campaign-report-exports.ts"],"names":[],"mappings":";;;AAIA,MAAa,qBAAqB;IACH;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,IAAI,CAAC,QAA4B,EAAE;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAA6B,KAAK,EAAE,6BAA6B,EAAE;YACzF,KAAK;SACN,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,GAAG,CAAC,EAAU;QACZ,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CACtB,KAAK,EACL,+BAA+B,kBAAkB,CAAC,EAAE,CAAC,EAAE,CACxD,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,KAAsC;QAC3C,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAuB,MAAM,EAAE,6BAA6B,EAAE;YACpF,IAAI,EAAE,KAAK;SACZ,CAAC,CAAC;IACL,CAAC;CACF;AAzBD,sDAyBC"}
@@ -0,0 +1,15 @@
1
+ import type { CampaignReport, GetCampaignReportQuery } from '../types/campaign';
2
+ import type { Page, PageQuery } from '../types/common';
3
+ import type { HttpClient } from '../http';
4
+ export declare class CampaignReports {
5
+ private readonly http;
6
+ constructor(http: HttpClient);
7
+ list(query?: Partial<PageQuery>): Promise<Page<CampaignReport>>;
8
+ /**
9
+ * The report for one campaign.
10
+ *
11
+ * `from`/`to` narrow the window the metrics are rolled up over; without them
12
+ * the report covers the campaign's whole life.
13
+ */
14
+ getByCampaign(campaignId: string, query?: GetCampaignReportQuery): Promise<CampaignReport>;
15
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CampaignReports = void 0;
4
+ class CampaignReports {
5
+ http;
6
+ constructor(http) {
7
+ this.http = http;
8
+ }
9
+ list(query = {}) {
10
+ return this.http.request('GET', '/v1/campaign-reports', { query });
11
+ }
12
+ /**
13
+ * The report for one campaign.
14
+ *
15
+ * `from`/`to` narrow the window the metrics are rolled up over; without them
16
+ * the report covers the campaign's whole life.
17
+ */
18
+ getByCampaign(campaignId, query = {}) {
19
+ return this.http.request('GET', `/v1/campaign-reports/${encodeURIComponent(campaignId)}`, {
20
+ query,
21
+ });
22
+ }
23
+ }
24
+ exports.CampaignReports = CampaignReports;
25
+ //# sourceMappingURL=campaign-reports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"campaign-reports.js","sourceRoot":"","sources":["../../src/resources/campaign-reports.ts"],"names":[],"mappings":";;;AAIA,MAAa,eAAe;IACG;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,IAAI,CAAC,QAA4B,EAAE;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAuB,KAAK,EAAE,sBAAsB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3F,CAAC;IAED;;;;;OAKG;IACH,aAAa,CAAC,UAAkB,EAAE,QAAgC,EAAE;QAClE,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAiB,KAAK,EAAE,wBAAwB,kBAAkB,CAAC,UAAU,CAAC,EAAE,EAAE;YACxG,KAAK;SACN,CAAC,CAAC;IACL,CAAC;CACF;AAlBD,0CAkBC"}
@@ -0,0 +1,239 @@
1
+ import type { HttpClient } from '../http';
2
+ import { InboxMedia } from './audience/inbox-media';
3
+ import { type CampaignReceiver, type ReceiverEncoding } from '../internal/receiver-file';
4
+ import { type BulkAudienceInput, type ResolvedAudience } from './campaign-audience';
5
+ import type { Campaign, CampaignNumbersFilter, CreateCampaignInput, ListCampaignsQuery, UpdateCampaignInput } from '../types/campaign';
6
+ import type { Page } from '../types/common';
7
+ /** Everything `createBulk` needs: a normal campaign, with an audience instead of `filters`. */
8
+ export interface CreateBulkCampaignInput extends Omit<CreateCampaignInput, 'filters' | 'type'> {
9
+ /** The recipients. Numbers, CSV text, parsed rows, or receivers you built. */
10
+ audience: BulkAudienceInput;
11
+ /** Overrides the dispatch route. Defaults to `bulk_campaign`; rarely needed. */
12
+ type?: string;
13
+ /** Aborts the upload. The campaign is not created if the upload is cancelled. */
14
+ signal?: AbortSignal;
15
+ }
16
+ /**
17
+ * What `createBulk` did, beyond creating the campaign.
18
+ *
19
+ * `receivers` is a **count**, deliberately matching what the same field means on
20
+ * a {@link Campaign} rather than carrying the resolved list. Two reasons: the
21
+ * caller already holds the data they passed in, and a name that means a number in
22
+ * one place and an array in another is the kind of thing that reads fine and then
23
+ * renders `[object Object]` on someone's screen.
24
+ */
25
+ export interface BulkCampaignResult extends Omit<ResolvedAudience, 'receivers'> {
26
+ campaign: Campaign;
27
+ /**
28
+ * Recipients actually written into the file.
29
+ *
30
+ * This -- not `campaign.receivers`, which is `0` until the campaign is sent --
31
+ * is the number to show a user now.
32
+ */
33
+ receivers: number;
34
+ /** Public URL of the encrypted receiver file, as stored on the campaign. */
35
+ fileUrl: string;
36
+ }
37
+ /** An uploader, so the receiver file's destination can be swapped or faked in tests. */
38
+ export type ReceiverUploader = Pick<InboxMedia, 'upload'>;
39
+ /**
40
+ * One bucket of a group campaign: a slice of the recipients, and what should
41
+ * send it.
42
+ *
43
+ * Which of `channel` and `pairIndex` applies depends on the campaign's
44
+ * `distributeBy`. Neither is validated here -- a bucket with the wrong one is a
45
+ * mistake the sending service will report, and guessing which the caller meant
46
+ * would be worse than passing it along.
47
+ */
48
+ export interface GroupBucket {
49
+ /** This bucket's recipients. Any of the four audience shapes. */
50
+ audience: BulkAudienceInput;
51
+ /** `distributeBy: 'channel'` -- the channel id this bucket is sent through. */
52
+ channel?: string;
53
+ /** `distributeBy: 'template'` -- which message in `messages` renders this bucket. */
54
+ pairIndex?: number;
55
+ /**
56
+ * Extra keys carried onto the filter entry verbatim, beside `numbers` and
57
+ * `hash`. The campaign contract passes anything it does not recognise straight
58
+ * through to the sending service, so this is the seam for distribution metadata
59
+ * this SDK does not model.
60
+ */
61
+ extra?: Record<string, unknown>;
62
+ }
63
+ export interface CreateGroupCampaignInput extends Omit<CreateCampaignInput, 'filters' | 'type'> {
64
+ /**
65
+ * What the buckets are split by.
66
+ *
67
+ * `channel` sends each bucket through a different channel; `template` renders
68
+ * each with a different message. Every bucket is stamped with this, and the
69
+ * contract refuses a campaign whose buckets disagree.
70
+ */
71
+ distributeBy: 'channel' | 'template';
72
+ buckets: readonly GroupBucket[];
73
+ /** Overrides the stored type. Defaults to `group_campaign`; rarely needed. */
74
+ type?: string;
75
+ signal?: AbortSignal;
76
+ }
77
+ /** One bucket's outcome. Same order as the buckets given. */
78
+ export interface GroupBucketResult {
79
+ receivers: number;
80
+ skipped: number;
81
+ duplicates: number;
82
+ /** Public URL of this bucket's encrypted file. */
83
+ fileUrl: string;
84
+ channel?: string;
85
+ pairIndex?: number;
86
+ }
87
+ export interface GroupCampaignResult {
88
+ campaign: Campaign;
89
+ /** Buckets that had recipients and were uploaded. Empty ones are not here. */
90
+ buckets: GroupBucketResult[];
91
+ /** Totals across every bucket. */
92
+ receivers: number;
93
+ skipped: number;
94
+ duplicates: number;
95
+ }
96
+ export declare class Campaigns {
97
+ private readonly http;
98
+ private readonly media;
99
+ constructor(http: HttpClient, media?: ReceiverUploader);
100
+ list(query?: Partial<ListCampaignsQuery>): Promise<Page<Campaign>>;
101
+ get(id: string): Promise<Campaign>;
102
+ /**
103
+ * Creates a campaign from a payload you have already assembled.
104
+ *
105
+ * For a bulk recipient list, prefer {@link createBulk} -- it does the
106
+ * encrypting and uploading that a file-backed audience requires, which is not
107
+ * something a caller should have to reproduce.
108
+ */
109
+ create(input: CreateCampaignInput): Promise<Campaign>;
110
+ /**
111
+ * Creates a campaign addressed to a list of numbers you supply directly.
112
+ *
113
+ * This is the "bulk campaign" path, and the SDK does the whole of it: the
114
+ * recipient list never travels as part of the campaign, because a million
115
+ * numbers is not a request body. Instead it is encrypted into a file, uploaded
116
+ * to storage, and the campaign stores a pointer to it -- which the sending
117
+ * service fetches and decrypts when the campaign runs.
118
+ *
119
+ * ```ts
120
+ * // Pasted numbers, no variables
121
+ * const { campaign, receivers } = await crm.campaigns.createBulk({
122
+ * name: 'Diwali offer',
123
+ * audience: { numbers: '919000000001, 919000000002\n919000000003' },
124
+ * messages: [{ channel: { id: channelId }, message: template }],
125
+ * });
126
+ *
127
+ * // A CSV, with every column available as a template variable
128
+ * await crm.campaigns.createBulk({
129
+ * name: 'Renewal reminder',
130
+ * audience: { csv: await file.text(), recipientColumn: 'phone' },
131
+ * messages: [{ channel: { id: channelId }, message: template }],
132
+ * trigger: 'schedule',
133
+ * scheduledAt: '2026-11-01T04:30:00.000Z',
134
+ * });
135
+ * ```
136
+ *
137
+ * ## What it does, in order
138
+ *
139
+ * 1. **Resolves the audience.** Numbers are reduced to digits; CSV or TSV text
140
+ * is parsed with quoted fields honoured; every column becomes a per-recipient
141
+ * variable. Rows with no usable number are skipped and duplicates dropped,
142
+ * both counted in the result rather than passed over in silence.
143
+ * 2. **Encrypts the list.** AES-256-CBC under a secret generated fresh for this
144
+ * one file. See `internal/receiver-file.ts` for the exact scheme.
145
+ * 3. **Uploads it** as a `document` through the media endpoint: a presigned URL
146
+ * is issued and the bytes go straight to storage, so the file never passes
147
+ * through the API.
148
+ * 4. **Creates the campaign** with `type: 'bulk_campaign'` and
149
+ * `filters: [{numbers: <url>, hash: <secret>}]`.
150
+ *
151
+ * ## Two things worth knowing
152
+ *
153
+ * **The returned `campaign.receivers` is `0`, and that is correct.** The
154
+ * audience is resolved when the campaign is sent, not when it is created. Use
155
+ * the `receivers` on this result -- the number actually written into the file --
156
+ * for anything you show the user now.
157
+ *
158
+ * **The file is readable by anyone holding both the URL and the `hash`.** The
159
+ * URL is a public storage link, so the secret stored alongside it on the
160
+ * campaign is the only thing protecting the list. It is a credential: do not log
161
+ * it, and do not copy a `filters` entry from one campaign into another system.
162
+ *
163
+ * Throws `validation_failed` if the audience resolves to no recipients, or to
164
+ * more than a million.
165
+ */
166
+ createBulk(input: CreateBulkCampaignInput): Promise<BulkCampaignResult>;
167
+ /**
168
+ * Creates a campaign whose recipients are split into buckets, each sent through
169
+ * a different channel or rendered with a different message.
170
+ *
171
+ * ```ts
172
+ * const { campaign, buckets, receivers } = await crm.campaigns.createGroup({
173
+ * name: 'Diwali offer',
174
+ * distributeBy: 'channel',
175
+ * buckets: [
176
+ * { channel: channelA, audience: { rows: north, recipientColumn: 'phone' } },
177
+ * { channel: channelB, audience: { rows: south, recipientColumn: 'phone' } },
178
+ * ],
179
+ * messages: [{ channel: { id: channelA }, message: template }],
180
+ * });
181
+ * ```
182
+ *
183
+ * ## How this differs from `createBulk`
184
+ *
185
+ * Three ways, and each of them is a thing that would be wrong if you built the
186
+ * payload by hand:
187
+ *
188
+ * - **One file per bucket, not one per campaign.** Each bucket is encrypted
189
+ * under its own secret and uploaded separately, and `filters` carries one
190
+ * entry per bucket. This is the only audience mode where `filters` has more
191
+ * than one element.
192
+ * - **A different file format.** A bucket file is a `||`-delimited table with a
193
+ * header row, not the JSON array a bulk file holds. The reading side chooses
194
+ * by campaign type, so the wrong encoding decrypts cleanly and then parses to
195
+ * nothing.
196
+ * - **Columns are fixed by the first recipient.** A table has one header, so
197
+ * every row carries the same variables in the same order. A bucket whose rows
198
+ * have differing columns will have the missing ones filled in as empty.
199
+ *
200
+ * Everything else is the same: numbers are reduced to digits, CSV is parsed
201
+ * with quoted fields honoured, duplicates and unusable rows are dropped and
202
+ * counted -- per bucket, and summed on the result.
203
+ *
204
+ * ## Empty buckets are dropped, not refused
205
+ *
206
+ * A bucket whose audience resolves to nobody is left out of `filters`
207
+ * altogether, matching the panel: distributing a list across six channels
208
+ * routinely leaves one or two with nothing in them, and that is not an error.
209
+ * The call fails only when **every** bucket is empty.
210
+ *
211
+ * De-duplication is per bucket. A number appearing in two buckets is sent to
212
+ * twice -- which is usually the intent, since the buckets are different
213
+ * channels, and is not something this can second-guess.
214
+ */
215
+ createGroup(input: CreateGroupCampaignInput): Promise<GroupCampaignResult>;
216
+ /**
217
+ * Encrypts and uploads a receiver list, answering with the audience filter it
218
+ * produced.
219
+ *
220
+ * The file half of {@link createBulk}, exposed because the two halves can fail
221
+ * for unrelated reasons. Holding the filter lets you retry a failed campaign
222
+ * creation -- or create several campaigns against one uploaded list -- without
223
+ * re-uploading, which matters when the list is large.
224
+ *
225
+ * The returned `hash` is the decryption secret. Keep it with the URL or the
226
+ * file becomes unreadable; there is no copy of it anywhere else.
227
+ */
228
+ uploadReceivers(receivers: readonly CampaignReceiver[], signal?: AbortSignal,
229
+ /**
230
+ * How the list is encoded inside the file. `json` for a bulk campaign's
231
+ * single file, `table` for a group campaign's bucket. The reading side picks
232
+ * by campaign type, so this has to match the campaign it is for.
233
+ */
234
+ encoding?: ReceiverEncoding): Promise<CampaignNumbersFilter>;
235
+ update(id: string, input: UpdateCampaignInput): Promise<Campaign>;
236
+ schedule(id: string, scheduledAt: string): Promise<Campaign>;
237
+ cancel(id: string): Promise<Campaign>;
238
+ delete(id: string): Promise<void>;
239
+ }
@@ -0,0 +1,300 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Campaigns = void 0;
4
+ const errors_1 = require("../errors");
5
+ const inbox_media_1 = require("./audience/inbox-media");
6
+ const receiver_file_1 = require("../internal/receiver-file");
7
+ const campaign_audience_1 = require("./campaign-audience");
8
+ /** The dispatch route a file-backed audience is sent under. */
9
+ const BULK_TYPE = 'bulk_campaign';
10
+ /**
11
+ * The dispatch route a bucketed audience is stored under.
12
+ *
13
+ * Stored as `group_campaign`; the campaign service relabels it to
14
+ * `bulk_campaign` on the hand-off to the sending service, because the two differ
15
+ * only in how many receiver files there are. Our own records keep the real name.
16
+ */
17
+ const GROUP_TYPE = 'group_campaign';
18
+ /** Buckets uploaded at once. Bounded so fifty buckets is not fifty open sockets. */
19
+ const UPLOAD_CONCURRENCY = 3;
20
+ class Campaigns {
21
+ http;
22
+ media;
23
+ constructor(http, media) {
24
+ this.http = http;
25
+ // Defaults to the audience service's own presigned-upload endpoint, which is
26
+ // the same path chat media takes.
27
+ this.media = media ?? new inbox_media_1.InboxMedia(http);
28
+ }
29
+ list(query = {}) {
30
+ return this.http.request('GET', '/v1/campaigns', { query });
31
+ }
32
+ get(id) {
33
+ return this.http.request('GET', `/v1/campaigns/${encodeURIComponent(id)}`);
34
+ }
35
+ /**
36
+ * Creates a campaign from a payload you have already assembled.
37
+ *
38
+ * For a bulk recipient list, prefer {@link createBulk} -- it does the
39
+ * encrypting and uploading that a file-backed audience requires, which is not
40
+ * something a caller should have to reproduce.
41
+ */
42
+ create(input) {
43
+ return this.http.request('POST', '/v1/campaigns', { body: input });
44
+ }
45
+ /**
46
+ * Creates a campaign addressed to a list of numbers you supply directly.
47
+ *
48
+ * This is the "bulk campaign" path, and the SDK does the whole of it: the
49
+ * recipient list never travels as part of the campaign, because a million
50
+ * numbers is not a request body. Instead it is encrypted into a file, uploaded
51
+ * to storage, and the campaign stores a pointer to it -- which the sending
52
+ * service fetches and decrypts when the campaign runs.
53
+ *
54
+ * ```ts
55
+ * // Pasted numbers, no variables
56
+ * const { campaign, receivers } = await crm.campaigns.createBulk({
57
+ * name: 'Diwali offer',
58
+ * audience: { numbers: '919000000001, 919000000002\n919000000003' },
59
+ * messages: [{ channel: { id: channelId }, message: template }],
60
+ * });
61
+ *
62
+ * // A CSV, with every column available as a template variable
63
+ * await crm.campaigns.createBulk({
64
+ * name: 'Renewal reminder',
65
+ * audience: { csv: await file.text(), recipientColumn: 'phone' },
66
+ * messages: [{ channel: { id: channelId }, message: template }],
67
+ * trigger: 'schedule',
68
+ * scheduledAt: '2026-11-01T04:30:00.000Z',
69
+ * });
70
+ * ```
71
+ *
72
+ * ## What it does, in order
73
+ *
74
+ * 1. **Resolves the audience.** Numbers are reduced to digits; CSV or TSV text
75
+ * is parsed with quoted fields honoured; every column becomes a per-recipient
76
+ * variable. Rows with no usable number are skipped and duplicates dropped,
77
+ * both counted in the result rather than passed over in silence.
78
+ * 2. **Encrypts the list.** AES-256-CBC under a secret generated fresh for this
79
+ * one file. See `internal/receiver-file.ts` for the exact scheme.
80
+ * 3. **Uploads it** as a `document` through the media endpoint: a presigned URL
81
+ * is issued and the bytes go straight to storage, so the file never passes
82
+ * through the API.
83
+ * 4. **Creates the campaign** with `type: 'bulk_campaign'` and
84
+ * `filters: [{numbers: <url>, hash: <secret>}]`.
85
+ *
86
+ * ## Two things worth knowing
87
+ *
88
+ * **The returned `campaign.receivers` is `0`, and that is correct.** The
89
+ * audience is resolved when the campaign is sent, not when it is created. Use
90
+ * the `receivers` on this result -- the number actually written into the file --
91
+ * for anything you show the user now.
92
+ *
93
+ * **The file is readable by anyone holding both the URL and the `hash`.** The
94
+ * URL is a public storage link, so the secret stored alongside it on the
95
+ * campaign is the only thing protecting the list. It is a credential: do not log
96
+ * it, and do not copy a `filters` entry from one campaign into another system.
97
+ *
98
+ * Throws `validation_failed` if the audience resolves to no recipients, or to
99
+ * more than a million.
100
+ */
101
+ async createBulk(input) {
102
+ const { audience, signal, ...campaign } = input;
103
+ const resolved = (0, campaign_audience_1.resolveAudience)(audience);
104
+ if (!resolved.receivers.length) {
105
+ throw new errors_1.CrmApiError(400, 'validation_failed', resolved.skipped > 0
106
+ ? `None of the ${resolved.skipped} entries supplied held a usable number`
107
+ : 'The audience resolved to no recipients');
108
+ }
109
+ if (resolved.receivers.length > campaign_audience_1.MAX_BULK_RECEIVERS) {
110
+ throw new errors_1.CrmApiError(400, 'validation_failed', `A bulk campaign may address at most ${campaign_audience_1.MAX_BULK_RECEIVERS.toLocaleString('en-US')} recipients; ` +
111
+ `this audience has ${resolved.receivers.length.toLocaleString('en-US')}. Split it across campaigns.`);
112
+ }
113
+ const filter = await this.uploadReceivers(resolved.receivers, signal);
114
+ const created = await this.create({
115
+ ...campaign,
116
+ type: input.type ?? BULK_TYPE,
117
+ filters: [filter],
118
+ });
119
+ return {
120
+ campaign: created,
121
+ receivers: resolved.receivers.length,
122
+ skipped: resolved.skipped,
123
+ duplicates: resolved.duplicates,
124
+ fileUrl: filter.numbers,
125
+ };
126
+ }
127
+ /**
128
+ * Creates a campaign whose recipients are split into buckets, each sent through
129
+ * a different channel or rendered with a different message.
130
+ *
131
+ * ```ts
132
+ * const { campaign, buckets, receivers } = await crm.campaigns.createGroup({
133
+ * name: 'Diwali offer',
134
+ * distributeBy: 'channel',
135
+ * buckets: [
136
+ * { channel: channelA, audience: { rows: north, recipientColumn: 'phone' } },
137
+ * { channel: channelB, audience: { rows: south, recipientColumn: 'phone' } },
138
+ * ],
139
+ * messages: [{ channel: { id: channelA }, message: template }],
140
+ * });
141
+ * ```
142
+ *
143
+ * ## How this differs from `createBulk`
144
+ *
145
+ * Three ways, and each of them is a thing that would be wrong if you built the
146
+ * payload by hand:
147
+ *
148
+ * - **One file per bucket, not one per campaign.** Each bucket is encrypted
149
+ * under its own secret and uploaded separately, and `filters` carries one
150
+ * entry per bucket. This is the only audience mode where `filters` has more
151
+ * than one element.
152
+ * - **A different file format.** A bucket file is a `||`-delimited table with a
153
+ * header row, not the JSON array a bulk file holds. The reading side chooses
154
+ * by campaign type, so the wrong encoding decrypts cleanly and then parses to
155
+ * nothing.
156
+ * - **Columns are fixed by the first recipient.** A table has one header, so
157
+ * every row carries the same variables in the same order. A bucket whose rows
158
+ * have differing columns will have the missing ones filled in as empty.
159
+ *
160
+ * Everything else is the same: numbers are reduced to digits, CSV is parsed
161
+ * with quoted fields honoured, duplicates and unusable rows are dropped and
162
+ * counted -- per bucket, and summed on the result.
163
+ *
164
+ * ## Empty buckets are dropped, not refused
165
+ *
166
+ * A bucket whose audience resolves to nobody is left out of `filters`
167
+ * altogether, matching the panel: distributing a list across six channels
168
+ * routinely leaves one or two with nothing in them, and that is not an error.
169
+ * The call fails only when **every** bucket is empty.
170
+ *
171
+ * De-duplication is per bucket. A number appearing in two buckets is sent to
172
+ * twice -- which is usually the intent, since the buckets are different
173
+ * channels, and is not something this can second-guess.
174
+ */
175
+ async createGroup(input) {
176
+ const { buckets, distributeBy, signal, ...campaign } = input;
177
+ const resolved = buckets.map((bucket) => ({ bucket, audience: (0, campaign_audience_1.resolveAudience)(bucket.audience) }));
178
+ const usable = resolved.filter((entry) => entry.audience.receivers.length > 0);
179
+ const totals = resolved.reduce((sum, entry) => ({
180
+ receivers: sum.receivers + entry.audience.receivers.length,
181
+ skipped: sum.skipped + entry.audience.skipped,
182
+ duplicates: sum.duplicates + entry.audience.duplicates,
183
+ }), { receivers: 0, skipped: 0, duplicates: 0 });
184
+ if (!usable.length) {
185
+ throw new errors_1.CrmApiError(400, 'validation_failed', buckets.length
186
+ ? `None of the ${buckets.length} buckets resolved to a recipient`
187
+ : 'A group campaign needs at least one bucket');
188
+ }
189
+ if (totals.receivers > campaign_audience_1.MAX_BULK_RECEIVERS) {
190
+ throw new errors_1.CrmApiError(400, 'validation_failed', `A campaign may address at most ${campaign_audience_1.MAX_BULK_RECEIVERS.toLocaleString('en-US')} recipients across all ` +
191
+ `buckets; this one has ${totals.receivers.toLocaleString('en-US')}. Split it across campaigns.`);
192
+ }
193
+ const uploaded = await mapWithLimit(usable, UPLOAD_CONCURRENCY, async ({ bucket, audience }) => {
194
+ const filter = await this.uploadReceivers(audience.receivers, signal, 'table');
195
+ return {
196
+ filter: {
197
+ ...bucket.extra,
198
+ type: distributeBy,
199
+ ...(bucket.channel === undefined ? {} : { channel: bucket.channel }),
200
+ ...(bucket.pairIndex === undefined ? {} : { pairIndex: bucket.pairIndex }),
201
+ // Carried because the panel carried it: the manager can read a bucket's
202
+ // size without downloading and decrypting the file to count it.
203
+ count: audience.receivers.length,
204
+ ...filter,
205
+ },
206
+ result: {
207
+ receivers: audience.receivers.length,
208
+ skipped: audience.skipped,
209
+ duplicates: audience.duplicates,
210
+ fileUrl: filter.numbers,
211
+ ...(bucket.channel === undefined ? {} : { channel: bucket.channel }),
212
+ ...(bucket.pairIndex === undefined ? {} : { pairIndex: bucket.pairIndex }),
213
+ },
214
+ };
215
+ });
216
+ const created = await this.create({
217
+ ...campaign,
218
+ type: input.type ?? GROUP_TYPE,
219
+ filters: uploaded.map((entry) => entry.filter),
220
+ });
221
+ return { campaign: created, buckets: uploaded.map((entry) => entry.result), ...totals };
222
+ }
223
+ /**
224
+ * Encrypts and uploads a receiver list, answering with the audience filter it
225
+ * produced.
226
+ *
227
+ * The file half of {@link createBulk}, exposed because the two halves can fail
228
+ * for unrelated reasons. Holding the filter lets you retry a failed campaign
229
+ * creation -- or create several campaigns against one uploaded list -- without
230
+ * re-uploading, which matters when the list is large.
231
+ *
232
+ * The returned `hash` is the decryption secret. Keep it with the URL or the
233
+ * file becomes unreadable; there is no copy of it anywhere else.
234
+ */
235
+ async uploadReceivers(receivers, signal,
236
+ /**
237
+ * How the list is encoded inside the file. `json` for a bulk campaign's
238
+ * single file, `table` for a group campaign's bucket. The reading side picks
239
+ * by campaign type, so this has to match the campaign it is for.
240
+ */
241
+ encoding = 'json') {
242
+ const file = await (0, receiver_file_1.buildReceiverFile)([...receivers], encoding);
243
+ const source = {
244
+ body: file.body,
245
+ filename: file.filename,
246
+ contentType: file.contentType,
247
+ size: file.size,
248
+ };
249
+ // `document` rather than a kind of its own: it is the one kind that accepts
250
+ // an arbitrary media type, and at 100MB its ceiling is far above a million
251
+ // encrypted numbers.
252
+ const numbers = await this.media.upload(source, {
253
+ kind: 'document',
254
+ filename: file.filename,
255
+ contentType: file.contentType,
256
+ signal,
257
+ });
258
+ return { numbers, hash: file.secret };
259
+ }
260
+ update(id, input) {
261
+ return this.http.request('PATCH', `/v1/campaigns/${encodeURIComponent(id)}`, { body: input });
262
+ }
263
+ schedule(id, scheduledAt) {
264
+ return this.http.request('POST', `/v1/campaigns/${encodeURIComponent(id)}/schedule`, {
265
+ body: { scheduledAt },
266
+ });
267
+ }
268
+ cancel(id) {
269
+ return this.http.request('POST', `/v1/campaigns/${encodeURIComponent(id)}/cancel`);
270
+ }
271
+ delete(id) {
272
+ return this.http.request('DELETE', `/v1/campaigns/${encodeURIComponent(id)}`);
273
+ }
274
+ }
275
+ exports.Campaigns = Campaigns;
276
+ /**
277
+ * `Promise.all` with a ceiling.
278
+ *
279
+ * A group campaign can have fifty buckets, and the panel uploaded them with an
280
+ * unbounded `Promise.all` -- fifty simultaneous uploads, which a browser queues
281
+ * anyway and a flaky connection turns into fifty retries at once. Three at a
282
+ * time finishes a realistic campaign in about the same wall-clock time without
283
+ * that.
284
+ *
285
+ * Results stay in input order, which `filters` depends on: a `template` bucket
286
+ * is addressed by its `pairIndex`, and reordering would silently re-point them.
287
+ */
288
+ const mapWithLimit = async (items, limit, run) => {
289
+ const results = new Array(items.length);
290
+ let next = 0;
291
+ const worker = async () => {
292
+ while (next < items.length) {
293
+ const index = next++;
294
+ results[index] = await run(items[index]);
295
+ }
296
+ };
297
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
298
+ return results;
299
+ };
300
+ //# sourceMappingURL=campaigns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"campaigns.js","sourceRoot":"","sources":["../../src/resources/campaigns.ts"],"names":[],"mappings":"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"}