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.
- package/README.md +62 -1
- package/dist/client.d.ts +24 -0
- package/dist/client.js +32 -5
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +20 -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/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/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-messages.d.ts +150 -0
- package/dist/resources/campaign-messages.js +172 -0
- package/dist/resources/campaign-messages.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 +82 -0
- package/dist/types/campaign.d.ts +319 -0
- package/dist/types/campaign.js +3 -0
- package/dist/types/campaign.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/docs/audience/notifications.md +263 -0
- package/docs/campaign/campaign-messages.md +273 -0
- package/docs/campaign/campaigns.md +449 -0
- package/package.json +1 -1
|
@@ -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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"}
|
package/dist/types/audience.d.ts
CHANGED
|
@@ -520,3 +520,85 @@ export interface WebhookEnvelope {
|
|
|
520
520
|
createdAt: IsoDate;
|
|
521
521
|
data: unknown;
|
|
522
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* Where a push notification can be delivered.
|
|
525
|
+
*
|
|
526
|
+
* It decides which half of the FCM payload the send path fills in -- `webpush`
|
|
527
|
+
* for a browser, `android`/`apns` for an app -- and it is the one thing about a
|
|
528
|
+
* device that cannot be inferred from its token. A registration token is an
|
|
529
|
+
* opaque string identifying a browser or app *instance*; nothing in it says what
|
|
530
|
+
* kind of thing that instance is.
|
|
531
|
+
*/
|
|
532
|
+
export type DevicePlatform = 'web' | 'android' | 'ios';
|
|
533
|
+
/**
|
|
534
|
+
* A subscribed device, as it reads back.
|
|
535
|
+
*
|
|
536
|
+
* The registration token is **never** returned. It is a send credential --
|
|
537
|
+
* anything holding one can push to that browser through the agency's Firebase
|
|
538
|
+
* project -- and the list exists so a user can recognise and revoke their own
|
|
539
|
+
* devices, which needs the label and the last-seen time and nothing more.
|
|
540
|
+
*/
|
|
541
|
+
export interface Device {
|
|
542
|
+
id: ObjectId;
|
|
543
|
+
platform: DevicePlatform;
|
|
544
|
+
/** Whatever `device_label` was sent on registration; `''` when none was. */
|
|
545
|
+
label: string;
|
|
546
|
+
/** The reseller frontend it registered from; `null` for the white-label one. */
|
|
547
|
+
clientId: string | null;
|
|
548
|
+
/**
|
|
549
|
+
* Last time a notification to this device succeeded, or it re-subscribed.
|
|
550
|
+
*
|
|
551
|
+
* The only signal that a device is still real: FCM does not revoke a token
|
|
552
|
+
* when an app is uninstalled or a browser's site data is cleared, it simply
|
|
553
|
+
* stops delivering to it. A device unseen for 60 days is pruned on that basis,
|
|
554
|
+
* which is another reason to call {@link Notifications.subscribe} on every app
|
|
555
|
+
* load rather than once.
|
|
556
|
+
*/
|
|
557
|
+
lastSeenAt: IsoDate;
|
|
558
|
+
createdAt: IsoDate;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* What `notifications.subscribe()` takes. Mirrors the wire body field for field.
|
|
562
|
+
*
|
|
563
|
+
* There is deliberately no user field. The acting user comes from the verified
|
|
564
|
+
* token the client was constructed with; a body that could name its own user
|
|
565
|
+
* would let any caller point someone else's notifications at their browser.
|
|
566
|
+
*/
|
|
567
|
+
export interface SubscribeNotificationsInput {
|
|
568
|
+
/**
|
|
569
|
+
* The FCM registration token for this browser or app instance, from
|
|
570
|
+
* `getToken()`. 20..4096 characters -- a sanity check rather than a format,
|
|
571
|
+
* since Google publishes no shape for these.
|
|
572
|
+
*/
|
|
573
|
+
token: string;
|
|
574
|
+
/**
|
|
575
|
+
* Defaults to `'web'` when the SDK can see a DOM, and is otherwise required:
|
|
576
|
+
* a React Native runtime has a `navigator` but no `document`, and guessing
|
|
577
|
+
* `'web'` there would send the wrong half of every payload.
|
|
578
|
+
*/
|
|
579
|
+
platform?: DevicePlatform;
|
|
580
|
+
/**
|
|
581
|
+
* What the user would recognise this device as, for the device list -- up to
|
|
582
|
+
* 200 characters. Left out, the SDK derives something like `Chrome on Windows`
|
|
583
|
+
* from the user agent when it is running in a browser. Cosmetic, never matched
|
|
584
|
+
* on.
|
|
585
|
+
*/
|
|
586
|
+
device_label?: string;
|
|
587
|
+
/**
|
|
588
|
+
* The reseller client this frontend belongs to (`tpc_…`), when there is one.
|
|
589
|
+
*
|
|
590
|
+
* Rarely needed: the client id the `CrmClient` already sends is verified and
|
|
591
|
+
* wins. Supplying a *different* one is refused with 403 rather than ignored.
|
|
592
|
+
* Omitted by the white-label frontend, which has no client.
|
|
593
|
+
*/
|
|
594
|
+
tpc_id?: string;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* `page` and `rows` only.
|
|
598
|
+
*
|
|
599
|
+
* The endpoint's schema accepts `search` because it shares the common list
|
|
600
|
+
* query, but the service does not apply it -- a device list is one person's few
|
|
601
|
+
* rows, so there is nothing to search. It is left out here rather than accepted
|
|
602
|
+
* and silently dropped.
|
|
603
|
+
*/
|
|
604
|
+
export type ListDevicesQuery = Pick<ListQuery, 'page' | 'rows'>;
|