ab-ecosystem-cpass 1.0.5 → 1.0.7

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 (42) hide show
  1. package/README.md +62 -1
  2. package/dist/client.d.ts +24 -0
  3. package/dist/client.js +32 -5
  4. package/dist/client.js.map +1 -1
  5. package/dist/index.d.ts +9 -0
  6. package/dist/index.js +20 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/internal/bytes.d.ts +38 -0
  9. package/dist/internal/bytes.js +84 -0
  10. package/dist/internal/bytes.js.map +1 -0
  11. package/dist/internal/device.d.ts +24 -0
  12. package/dist/internal/device.js +91 -0
  13. package/dist/internal/device.js.map +1 -0
  14. package/dist/internal/md5.d.ts +4 -0
  15. package/dist/internal/md5.js +109 -0
  16. package/dist/internal/md5.js.map +1 -0
  17. package/dist/internal/receiver-file.d.ts +106 -0
  18. package/dist/internal/receiver-file.js +128 -0
  19. package/dist/internal/receiver-file.js.map +1 -0
  20. package/dist/resources/audience/notifications.d.ts +171 -0
  21. package/dist/resources/audience/notifications.js +215 -0
  22. package/dist/resources/audience/notifications.js.map +1 -0
  23. package/dist/resources/campaign-audience.d.ts +132 -0
  24. package/dist/resources/campaign-audience.js +224 -0
  25. package/dist/resources/campaign-audience.js.map +1 -0
  26. package/dist/resources/campaign-messages.d.ts +150 -0
  27. package/dist/resources/campaign-messages.js +172 -0
  28. package/dist/resources/campaign-messages.js.map +1 -0
  29. package/dist/resources/campaigns.d.ts +239 -0
  30. package/dist/resources/campaigns.js +300 -0
  31. package/dist/resources/campaigns.js.map +1 -0
  32. package/dist/types/audience.d.ts +82 -0
  33. package/dist/types/campaign.d.ts +319 -0
  34. package/dist/types/campaign.js +3 -0
  35. package/dist/types/campaign.js.map +1 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/index.js +2 -0
  38. package/dist/types/index.js.map +1 -1
  39. package/docs/audience/notifications.md +263 -0
  40. package/docs/campaign/campaign-messages.md +273 -0
  41. package/docs/campaign/campaigns.md +449 -0
  42. package/package.json +1 -1
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CampaignMessages = void 0;
4
+ const BASE = '/v1/campaign-messages';
5
+ /**
6
+ * Campaign reporting: what actually happened to each message.
7
+ *
8
+ * ```ts
9
+ * const stats = await crm.campaignMessages.stats({ campaign });
10
+ * // { total: 12480, sent: 12410, delivered: 11980, read: 7420, replied: 310, failed: 70 }
11
+ *
12
+ * const { data } = await crm.campaignMessages.list({ campaign, status: 'failed' });
13
+ * data[0].error; // why that one did not go
14
+ * ```
15
+ *
16
+ * ## Where these numbers come from
17
+ *
18
+ * One row per attempted send, written by the campaign manager as it hands each
19
+ * message to a channel, with its `status` moved forward by the channel's
20
+ * delivery webhooks -- which are processed outside the CRM entirely. The report
21
+ * is **counted off those rows when you ask for it**, not rolled up on a
22
+ * schedule, so there is no "generated at" to reason about and no staleness
23
+ * window: the numbers are as current as the webhooks that have arrived.
24
+ *
25
+ * The flip side is that a campaign still going out has moving numbers. Reading
26
+ * `stats` seconds after launching under-counts everything, and that is correct
27
+ * rather than a lag.
28
+ *
29
+ * ## The funnel
30
+ *
31
+ * `status` is a single field holding the furthest stage reached, so the counts
32
+ * are cumulative and nest: `replied ⊆ read ⊆ delivered ⊆ sent`. For a stage's
33
+ * exclusive count, subtract the next one -- `delivered - read` is "delivered but
34
+ * unread". `failed` is outside the funnel and never inside `sent`.
35
+ *
36
+ * ## There is no export endpoint
37
+ *
38
+ * An export is {@link list} written to a file, so {@link iterate} does the
39
+ * paging and the caller formats the rows. Nothing is queued, nothing has to be
40
+ * polled, and no generated file expires before someone downloads it.
41
+ */
42
+ class CampaignMessages {
43
+ http;
44
+ constructor(http) {
45
+ this.http = http;
46
+ }
47
+ /**
48
+ * The campaign's funnel counts.
49
+ *
50
+ * `GET /v1/campaign-messages/stats?campaign=<id>` · `campaigns:read` · **200**
51
+ *
52
+ * ### Query
53
+ *
54
+ * | Field | Type | Required | Notes |
55
+ * | --- | --- | --- | --- |
56
+ * | `campaign` | string | yes | The campaign id. |
57
+ * | `message` | number | no | One message of a multi-message campaign, zero-based. |
58
+ *
59
+ * ```ts
60
+ * const overall = await crm.campaignMessages.stats({ campaign });
61
+ * const second = await crm.campaignMessages.stats({ campaign, message: 1 });
62
+ * ```
63
+ *
64
+ * No date window: a campaign is one send, so its whole life is the only
65
+ * meaningful window. A repeating campaign's runs are separate campaigns with
66
+ * their own ids.
67
+ *
68
+ * `404 not_found` if the campaign is not the caller's -- the same answer as a
69
+ * campaign that does not exist, so the error cannot be used to probe ids.
70
+ */
71
+ stats(query) {
72
+ return this.http.request('GET', `${BASE}/stats`, { query });
73
+ }
74
+ /**
75
+ * The rows behind the funnel. This is the report table.
76
+ *
77
+ * `GET /v1/campaign-messages?campaign=<id>` · `campaigns:read` · **200**
78
+ *
79
+ * ### Query
80
+ *
81
+ * | Field | Type | Default | Notes |
82
+ * | --- | --- | --- | --- |
83
+ * | `campaign` | string | — | Required. |
84
+ * | `message` | number | — | Zero-based index of one campaign message. |
85
+ * | `status` | `sent \| delivered \| read \| replied \| failed` | — | Cumulative; see below. Omit for every row. |
86
+ * | `exact` | boolean | `false` | Only rows sitting at `status` right now. |
87
+ * | `identifier` | string | — | Exact match on one recipient. Not a search. |
88
+ * | `reply_type` | string | — | Replied rows only; refused otherwise. |
89
+ * | `replied_within_hours` | number | — | Replied rows only; refused otherwise. |
90
+ * | `page` | number | `1` | 1-based. |
91
+ * | `rows` | number | `25` | 1–100. |
92
+ *
93
+ * Newest attempt first, ordered by when the row was written -- not by a stage
94
+ * timestamp, which would reorder the list as webhooks arrive and make paging
95
+ * skip and repeat rows.
96
+ *
97
+ * ```ts
98
+ * // the two queries a report screen actually makes
99
+ * const failures = await crm.campaignMessages.list({ campaign, status: 'failed', rows: 100 });
100
+ * const unread = await crm.campaignMessages.list({ campaign, status: 'delivered', exact: true });
101
+ * ```
102
+ *
103
+ * `reply_type` and `replied_within_hours` are **refused** with
104
+ * `400 validation_failed` unless `status` is `replied`, rather than quietly
105
+ * matching nothing.
106
+ */
107
+ list(query) {
108
+ return this.http.request('GET', BASE, { query });
109
+ }
110
+ /**
111
+ * Every matching row, paged through for you. What an export is.
112
+ *
113
+ * ```ts
114
+ * for await (const row of crm.campaignMessages.iterate({ campaign })) {
115
+ * csv.write([row.identifier, row.name, row.status, row.error ?? '']);
116
+ * }
117
+ * ```
118
+ *
119
+ * Pages at 100 rows unless told otherwise -- the server's maximum, and the
120
+ * fewest round trips. A campaign of a million recipients is ten thousand
121
+ * requests, so for exports that size prefer running it where the latency is
122
+ * low rather than from a browser.
123
+ *
124
+ * The collection is live: rows are written while a campaign is going out, and
125
+ * statuses change under an export that takes minutes. Offset paging over
126
+ * moving data can repeat or skip a row -- for a stable snapshot, export after
127
+ * the campaign completes.
128
+ */
129
+ async *iterate(query) {
130
+ const rows = query.rows ?? 100;
131
+ let page = query.page ?? 1;
132
+ for (;;) {
133
+ const result = await this.list({ ...query, page, rows });
134
+ for (const record of result.data)
135
+ yield record;
136
+ if (result.data.length === 0 || page >= result.pagination.pages)
137
+ return;
138
+ page += 1;
139
+ }
140
+ }
141
+ /**
142
+ * What people replied, grouped by the reply itself.
143
+ *
144
+ * `GET /v1/campaign-messages/replies?campaign=<id>` · `campaigns:read` · **200**
145
+ *
146
+ * ### Query
147
+ *
148
+ * | Field | Type | Default | Notes |
149
+ * | --- | --- | --- | --- |
150
+ * | `campaign` | string | — | Required. |
151
+ * | `message` | number | — | Zero-based index of one campaign message. |
152
+ * | `limit` | number | `50` | Distinct replies to return, 1–200. |
153
+ *
154
+ * ```ts
155
+ * const { groups, replied, distinct } = await crm.campaignMessages.replies({ campaign });
156
+ * groups; // [{ reply: 'Yes', replyType: 'button', count: 182 }, …]
157
+ * ```
158
+ *
159
+ * This is what a poll or a button message is sent to find out. Grouped on the
160
+ * reply **and** its type, so a tapped `Yes` and a typed `Yes` stay apart --
161
+ * they say different things about how engaged the recipient was.
162
+ *
163
+ * `distinct` against `groups.length` tells you whether the breakdown is
164
+ * complete. A campaign that asked an open question has as many groups as it
165
+ * has repliers, so a truncated answer there is normal rather than a problem.
166
+ */
167
+ replies(query) {
168
+ return this.http.request('GET', `${BASE}/replies`, { query });
169
+ }
170
+ }
171
+ exports.CampaignMessages = CampaignMessages;
172
+ //# sourceMappingURL=campaign-messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"campaign-messages.js","sourceRoot":"","sources":["../../src/resources/campaign-messages.ts"],"names":[],"mappings":";;;AAWA,MAAM,IAAI,GAAG,uBAAuB,CAAC;AAErC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAa,gBAAgB;IACE;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,KAAK,CAAC,KAAgC;QACpC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAuB,KAAK,EAAE,GAAG,IAAI,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACpF,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,IAAI,CAAC,KAAgC;QACnC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAmC,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACrF,CAAC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,KAAK,CAAC,CAAC,OAAO,CAAC,KAAgC;QAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,GAAG,CAAC;QAC/B,IAAI,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;QAE3B,SAAS,CAAC;YACR,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACzD,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,IAAI;gBAAE,MAAM,MAAM,CAAC;YAE/C,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK;gBAAE,OAAO;YACxE,IAAI,IAAI,CAAC,CAAC;QACZ,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,OAAO,CAAC,KAA2B;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAyB,KAAK,EAAE,GAAG,IAAI,UAAU,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACxF,CAAC;CACF;AAjID,4CAiIC"}
@@ -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
+ }