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,24 @@
|
|
|
1
|
+
import type { DevicePlatform } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* A label a person would recognise their own browser by, e.g. `Chrome on Windows`.
|
|
4
|
+
*
|
|
5
|
+
* `undefined` rather than a placeholder when there is nothing to go on -- the
|
|
6
|
+
* field is optional, and an omitted label reads back as `''`, which a UI can
|
|
7
|
+
* render as "this device" far better than a string like `Unknown on Unknown`
|
|
8
|
+
* can.
|
|
9
|
+
*
|
|
10
|
+
* Only the family and the system, never the version. A version would change the
|
|
11
|
+
* label every few weeks on a device that has not changed, and the label exists
|
|
12
|
+
* to be recognised rather than to be precise.
|
|
13
|
+
*/
|
|
14
|
+
export declare const describeDevice: () => string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* `'web'` when there is a DOM, `undefined` otherwise.
|
|
17
|
+
*
|
|
18
|
+
* Keyed on `document` and not on `navigator`, which is the distinction that
|
|
19
|
+
* matters: React Native defines a `navigator` but no `document`, so a
|
|
20
|
+
* navigator-based check would quietly register every mobile device as a browser
|
|
21
|
+
* and the send path would fill in the `webpush` block instead of `android`/`apns`
|
|
22
|
+
* -- a notification that is accepted by FCM and never shown.
|
|
23
|
+
*/
|
|
24
|
+
export declare const detectPlatform: () => DevicePlatform | undefined;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectPlatform = exports.describeDevice = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* What the current runtime is, for the two fields a caller should not have to
|
|
6
|
+
* fill in by hand.
|
|
7
|
+
*
|
|
8
|
+
* Both functions here are best-effort and both are cosmetic-or-defaultable:
|
|
9
|
+
* nothing in the send path depends on getting the label right, and a platform
|
|
10
|
+
* that cannot be detected is asked for rather than guessed.
|
|
11
|
+
*
|
|
12
|
+
* ## Why this is not the `user-agents` package
|
|
13
|
+
*
|
|
14
|
+
* The legacy panel sent its `device` field as `new UserAgent().data` from the
|
|
15
|
+
* `user-agents` npm package. That package **generates a random plausible user
|
|
16
|
+
* agent**; it does not read the browser's own. So every device row ever written
|
|
17
|
+
* by the old panel describes a machine that does not exist, which is why the
|
|
18
|
+
* replacement stores a caller-supplied label instead of a parsed object, and why
|
|
19
|
+
* the default below reads `navigator.userAgent` directly.
|
|
20
|
+
*/
|
|
21
|
+
/** The browser's own user agent, or `undefined` outside a browser. */
|
|
22
|
+
const userAgent = () => {
|
|
23
|
+
const nav = globalThis.navigator;
|
|
24
|
+
return typeof nav?.userAgent === 'string' && nav.userAgent ? nav.userAgent : undefined;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Browser families, **most specific first**.
|
|
28
|
+
*
|
|
29
|
+
* The order is the whole correctness of this table: Edge and Opera both carry
|
|
30
|
+
* `Chrome/` in their user agent, and every Chromium browser carries `Safari/`.
|
|
31
|
+
* Matching in declaration order is what keeps Edge from reading as Chrome and
|
|
32
|
+
* Chrome from reading as Safari.
|
|
33
|
+
*/
|
|
34
|
+
const BROWSERS = [
|
|
35
|
+
[/\bEdg(?:e|A|iOS)?\//, 'Edge'],
|
|
36
|
+
[/\bOPR\/|\bOpera\//, 'Opera'],
|
|
37
|
+
[/\bSamsungBrowser\//, 'Samsung Internet'],
|
|
38
|
+
[/\bFirefox\/|\bFxiOS\//, 'Firefox'],
|
|
39
|
+
// Chrome on iOS is `CriOS`: it is Safari's engine wearing Chrome's name, and
|
|
40
|
+
// the name is what a user would recognise in a device list.
|
|
41
|
+
[/\bChrome\/|\bCriOS\//, 'Chrome'],
|
|
42
|
+
[/\bSafari\//, 'Safari'],
|
|
43
|
+
];
|
|
44
|
+
/** Operating systems, also most specific first -- an Android UA also says `Linux`. */
|
|
45
|
+
const SYSTEMS = [
|
|
46
|
+
[/\bWindows\b/, 'Windows'],
|
|
47
|
+
[/\bAndroid\b/, 'Android'],
|
|
48
|
+
[/\b(?:iPhone|iPad|iPod)\b/, 'iOS'],
|
|
49
|
+
[/\bCrOS\b/, 'ChromeOS'],
|
|
50
|
+
[/\bMac OS X\b|\bMacintosh\b/, 'macOS'],
|
|
51
|
+
[/\bLinux\b/, 'Linux'],
|
|
52
|
+
];
|
|
53
|
+
const match = (table, ua) => table.find(([pattern]) => pattern.test(ua))?.[1];
|
|
54
|
+
/**
|
|
55
|
+
* A label a person would recognise their own browser by, e.g. `Chrome on Windows`.
|
|
56
|
+
*
|
|
57
|
+
* `undefined` rather than a placeholder when there is nothing to go on -- the
|
|
58
|
+
* field is optional, and an omitted label reads back as `''`, which a UI can
|
|
59
|
+
* render as "this device" far better than a string like `Unknown on Unknown`
|
|
60
|
+
* can.
|
|
61
|
+
*
|
|
62
|
+
* Only the family and the system, never the version. A version would change the
|
|
63
|
+
* label every few weeks on a device that has not changed, and the label exists
|
|
64
|
+
* to be recognised rather than to be precise.
|
|
65
|
+
*/
|
|
66
|
+
const describeDevice = () => {
|
|
67
|
+
const ua = userAgent();
|
|
68
|
+
if (!ua)
|
|
69
|
+
return undefined;
|
|
70
|
+
const browser = match(BROWSERS, ua);
|
|
71
|
+
const system = match(SYSTEMS, ua);
|
|
72
|
+
if (browser && system)
|
|
73
|
+
return `${browser} on ${system}`;
|
|
74
|
+
return browser ?? system;
|
|
75
|
+
};
|
|
76
|
+
exports.describeDevice = describeDevice;
|
|
77
|
+
/**
|
|
78
|
+
* `'web'` when there is a DOM, `undefined` otherwise.
|
|
79
|
+
*
|
|
80
|
+
* Keyed on `document` and not on `navigator`, which is the distinction that
|
|
81
|
+
* matters: React Native defines a `navigator` but no `document`, so a
|
|
82
|
+
* navigator-based check would quietly register every mobile device as a browser
|
|
83
|
+
* and the send path would fill in the `webpush` block instead of `android`/`apns`
|
|
84
|
+
* -- a notification that is accepted by FCM and never shown.
|
|
85
|
+
*/
|
|
86
|
+
const detectPlatform = () => typeof globalThis.document === 'object' &&
|
|
87
|
+
globalThis.document !== null
|
|
88
|
+
? 'web'
|
|
89
|
+
: undefined;
|
|
90
|
+
exports.detectPlatform = detectPlatform;
|
|
91
|
+
//# sourceMappingURL=device.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.js","sourceRoot":"","sources":["../../src/internal/device.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,sEAAsE;AACtE,MAAM,SAAS,GAAG,GAAuB,EAAE;IACzC,MAAM,GAAG,GAAI,UAAsD,CAAC,SAAS,CAAC;IAC9E,OAAO,OAAO,GAAG,EAAE,SAAS,KAAK,QAAQ,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;AACzF,CAAC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,QAAQ,GAAgC;IAC5C,CAAC,qBAAqB,EAAE,MAAM,CAAC;IAC/B,CAAC,mBAAmB,EAAE,OAAO,CAAC;IAC9B,CAAC,oBAAoB,EAAE,kBAAkB,CAAC;IAC1C,CAAC,uBAAuB,EAAE,SAAS,CAAC;IACpC,6EAA6E;IAC7E,4DAA4D;IAC5D,CAAC,sBAAsB,EAAE,QAAQ,CAAC;IAClC,CAAC,YAAY,EAAE,QAAQ,CAAC;CACzB,CAAC;AAEF,sFAAsF;AACtF,MAAM,OAAO,GAAgC;IAC3C,CAAC,aAAa,EAAE,SAAS,CAAC;IAC1B,CAAC,aAAa,EAAE,SAAS,CAAC;IAC1B,CAAC,0BAA0B,EAAE,KAAK,CAAC;IACnC,CAAC,UAAU,EAAE,UAAU,CAAC;IACxB,CAAC,4BAA4B,EAAE,OAAO,CAAC;IACvC,CAAC,WAAW,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,KAAK,GAAG,CAAC,KAAkC,EAAE,EAAU,EAAsB,EAAE,CACnF,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAEnD;;;;;;;;;;;GAWG;AACI,MAAM,cAAc,GAAG,GAAuB,EAAE;IACrD,MAAM,EAAE,GAAG,SAAS,EAAE,CAAC;IACvB,IAAI,CAAC,EAAE;QAAE,OAAO,SAAS,CAAC;IAE1B,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACpC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAElC,IAAI,OAAO,IAAI,MAAM;QAAE,OAAO,GAAG,OAAO,OAAO,MAAM,EAAE,CAAC;IACxD,OAAO,OAAO,IAAI,MAAM,CAAC;AAC3B,CAAC,CAAC;AATW,QAAA,cAAc,kBASzB;AAEF;;;;;;;;GAQG;AACI,MAAM,cAAc,GAAG,GAA+B,EAAE,CAC7D,OAAQ,UAAqC,CAAC,QAAQ,KAAK,QAAQ;IAClE,UAAqC,CAAC,QAAQ,KAAK,IAAI;IACtD,CAAC,CAAC,KAAK;IACP,CAAC,CAAC,SAAS,CAAC;AAJH,QAAA,cAAc,kBAIX"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.md5 = exports.md5Bytes = void 0;
|
|
4
|
+
const bytes_1 = require("./bytes");
|
|
5
|
+
/**
|
|
6
|
+
* MD5, because the receiver file's key derivation uses it and no platform offers
|
|
7
|
+
* it.
|
|
8
|
+
*
|
|
9
|
+
* Web Crypto deliberately does not implement MD5 -- it is broken for every
|
|
10
|
+
* purpose it was designed for -- and Node's `crypto.createHash('md5')` does not
|
|
11
|
+
* exist in a browser. So it is written out here.
|
|
12
|
+
*
|
|
13
|
+
* **This is not a security choice, it is a compatibility requirement.** The
|
|
14
|
+
* campaign manager decrypts these files with `md5(secret)` as the AES key, and
|
|
15
|
+
* that algorithm is fixed on the reading side. Changing it here would produce
|
|
16
|
+
* files nothing can read. See `receiver-file.ts`, which documents the whole
|
|
17
|
+
* scheme and its weaknesses in one place.
|
|
18
|
+
*
|
|
19
|
+
* Not exported from the package: nothing outside the receiver file should reach
|
|
20
|
+
* for an MD5.
|
|
21
|
+
*/
|
|
22
|
+
/** Per-round left-rotation amounts. */
|
|
23
|
+
const SHIFTS = [
|
|
24
|
+
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
|
|
25
|
+
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
|
|
26
|
+
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
|
|
27
|
+
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* `floor(abs(sin(i + 1)) * 2^32)`, the constants from RFC 1321.
|
|
31
|
+
*
|
|
32
|
+
* Computed rather than transcribed: sixty-four hand-typed 32-bit hex literals is
|
|
33
|
+
* a place to make a silent mistake, and the derivation is exact in IEEE-754 for
|
|
34
|
+
* every one of these values. The test vectors in RFC 1321 are what confirm it.
|
|
35
|
+
*/
|
|
36
|
+
const K = (() => {
|
|
37
|
+
const table = new Uint32Array(64);
|
|
38
|
+
for (let i = 0; i < 64; i += 1)
|
|
39
|
+
table[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 2 ** 32);
|
|
40
|
+
return table;
|
|
41
|
+
})();
|
|
42
|
+
const rotl = (value, bits) => (value << bits) | (value >>> (32 - bits));
|
|
43
|
+
/** MD5 of raw bytes, as lower-case hex. */
|
|
44
|
+
const md5Bytes = (input) => {
|
|
45
|
+
const bitLength = input.length * 8;
|
|
46
|
+
// One 0x80 byte, then zeroes, then the bit length as a little-endian u64 --
|
|
47
|
+
// padded so the total is a whole number of 64-byte blocks.
|
|
48
|
+
const blocks = Math.floor((input.length + 8) / 64) + 1;
|
|
49
|
+
const padded = new Uint8Array(blocks * 64);
|
|
50
|
+
padded.set(input);
|
|
51
|
+
padded[input.length] = 0x80;
|
|
52
|
+
const view = new DataView(padded.buffer);
|
|
53
|
+
view.setUint32(padded.length - 8, bitLength >>> 0, true);
|
|
54
|
+
view.setUint32(padded.length - 4, Math.floor(bitLength / 0x1_0000_0000), true);
|
|
55
|
+
let h0 = 0x67452301;
|
|
56
|
+
let h1 = 0xefcdab89;
|
|
57
|
+
let h2 = 0x98badcfe;
|
|
58
|
+
let h3 = 0x10325476;
|
|
59
|
+
const words = new Uint32Array(16);
|
|
60
|
+
for (let block = 0; block < blocks; block += 1) {
|
|
61
|
+
const offset = block * 64;
|
|
62
|
+
for (let i = 0; i < 16; i += 1)
|
|
63
|
+
words[i] = view.getUint32(offset + i * 4, true);
|
|
64
|
+
let a = h0;
|
|
65
|
+
let b = h1;
|
|
66
|
+
let c = h2;
|
|
67
|
+
let d = h3;
|
|
68
|
+
for (let i = 0; i < 64; i += 1) {
|
|
69
|
+
let f;
|
|
70
|
+
let g;
|
|
71
|
+
if (i < 16) {
|
|
72
|
+
f = (b & c) | (~b & d);
|
|
73
|
+
g = i;
|
|
74
|
+
}
|
|
75
|
+
else if (i < 32) {
|
|
76
|
+
f = (d & b) | (~d & c);
|
|
77
|
+
g = (5 * i + 1) % 16;
|
|
78
|
+
}
|
|
79
|
+
else if (i < 48) {
|
|
80
|
+
f = b ^ c ^ d;
|
|
81
|
+
g = (3 * i + 5) % 16;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
f = c ^ (b | ~d);
|
|
85
|
+
g = (7 * i) % 16;
|
|
86
|
+
}
|
|
87
|
+
const rotated = rotl((a + f + K[i] + words[g]) | 0, SHIFTS[i]);
|
|
88
|
+
a = d;
|
|
89
|
+
d = c;
|
|
90
|
+
c = b;
|
|
91
|
+
b = (b + rotated) | 0;
|
|
92
|
+
}
|
|
93
|
+
h0 = (h0 + a) | 0;
|
|
94
|
+
h1 = (h1 + b) | 0;
|
|
95
|
+
h2 = (h2 + c) | 0;
|
|
96
|
+
h3 = (h3 + d) | 0;
|
|
97
|
+
}
|
|
98
|
+
const digest = new Uint8Array(16);
|
|
99
|
+
new DataView(digest.buffer).setUint32(0, h0 >>> 0, true);
|
|
100
|
+
new DataView(digest.buffer).setUint32(4, h1 >>> 0, true);
|
|
101
|
+
new DataView(digest.buffer).setUint32(8, h2 >>> 0, true);
|
|
102
|
+
new DataView(digest.buffer).setUint32(12, h3 >>> 0, true);
|
|
103
|
+
return (0, bytes_1.toHex)(digest);
|
|
104
|
+
};
|
|
105
|
+
exports.md5Bytes = md5Bytes;
|
|
106
|
+
/** MD5 of a string's UTF-8 bytes, as lower-case hex -- what `md5(value)` on npm returns. */
|
|
107
|
+
const md5 = (value) => (0, exports.md5Bytes)((0, bytes_1.utf8)(value));
|
|
108
|
+
exports.md5 = md5;
|
|
109
|
+
//# sourceMappingURL=md5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"md5.js","sourceRoot":"","sources":["../../src/internal/md5.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** One recipient and the variables their message is rendered with. */
|
|
2
|
+
export interface CampaignReceiver {
|
|
3
|
+
/** The recipient's number, digits only, no `+` and no separators. */
|
|
4
|
+
waba: string;
|
|
5
|
+
/**
|
|
6
|
+
* Per-recipient template variables, as the manager expects them: a flat list of
|
|
7
|
+
* `{id, value}` rather than an object, because the ids come from CSV headers
|
|
8
|
+
* and are not safe as object keys.
|
|
9
|
+
*/
|
|
10
|
+
custom_fields: {
|
|
11
|
+
id: string;
|
|
12
|
+
value: string;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
/** An encrypted receiver file, ready to upload. */
|
|
16
|
+
export interface ReceiverFile {
|
|
17
|
+
/** Base64 AES ciphertext. This is the entire file body. */
|
|
18
|
+
body: string;
|
|
19
|
+
/** The secret the file was encrypted under. Stored on the campaign as `hash`. */
|
|
20
|
+
secret: string;
|
|
21
|
+
filename: string;
|
|
22
|
+
contentType: 'text/plain';
|
|
23
|
+
/** Byte length of `body`, which the upload ticket is signed against. */
|
|
24
|
+
size: number;
|
|
25
|
+
/** How many recipients the file contains. */
|
|
26
|
+
receivers: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A fresh secret for one receiver file.
|
|
30
|
+
*
|
|
31
|
+
* Sixteen random bytes as hex -- thirty-two characters, which is the shape
|
|
32
|
+
* already in the field and what the `hash` field on a stored campaign holds.
|
|
33
|
+
*
|
|
34
|
+
* The legacy frontend generated this as 16 random bytes, hex-encoded, then
|
|
35
|
+
* applied `.toString('base64url').replace(/_/gi,'U').replace(/-/gi,'D')`. That
|
|
36
|
+
* chain is a **no-op**: `String.prototype.toString` ignores its argument and
|
|
37
|
+
* returns the string unchanged, and the hex alphabet contains no `_` or `-` for
|
|
38
|
+
* the replacements to find. So the value that reaches the manager has always been
|
|
39
|
+
* plain hex, and this produces the identical thing without the dead code.
|
|
40
|
+
*
|
|
41
|
+
* One secret per file, never reused. That is what keeps the fixed IV above from
|
|
42
|
+
* being the flaw it would otherwise be.
|
|
43
|
+
*/
|
|
44
|
+
export declare const generateReceiverSecret: () => string;
|
|
45
|
+
/**
|
|
46
|
+
* Encrypts the receiver list exactly as the campaign manager expects to read it.
|
|
47
|
+
*
|
|
48
|
+
* ```
|
|
49
|
+
* key = md5(secret) -- 32 lower-case hex characters
|
|
50
|
+
* key bytes = the ASCII bytes of that hex -- 32 bytes, so AES-256
|
|
51
|
+
* iv = ASCII bytes of '678025308de70905'
|
|
52
|
+
* ciphertext = AES-256-CBC, PKCS#7 padded
|
|
53
|
+
* file body = base64(ciphertext)
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* Two parts of that are counter-intuitive and both are deliberate, because they
|
|
57
|
+
* mirror what `CryptoJS.AES.encrypt(text, keyWordArray, {iv})` does:
|
|
58
|
+
*
|
|
59
|
+
* - **The key is the hex *text*, not the digest bytes.** `md5()` returns a
|
|
60
|
+
* 32-character string and it is parsed as UTF-8, which lands on 32 bytes and
|
|
61
|
+
* therefore AES-256. Using the 16 raw digest bytes instead would be AES-128 and
|
|
62
|
+
* would not decrypt.
|
|
63
|
+
* - **There is no `Salted__` header.** CryptoJS only writes one when it derives a
|
|
64
|
+
* key from a passphrase. Given a key directly, as here, its default formatter
|
|
65
|
+
* emits bare base64 of the ciphertext.
|
|
66
|
+
*
|
|
67
|
+
* Web Crypto applies PKCS#7 padding to AES-CBC itself, which is the same padding
|
|
68
|
+
* CryptoJS defaults to -- so the outputs are byte-identical.
|
|
69
|
+
*/
|
|
70
|
+
export declare const encryptReceivers: (plaintext: string, secret: string) => Promise<string>;
|
|
71
|
+
/**
|
|
72
|
+
* The two plaintext encodings a receiver file can hold.
|
|
73
|
+
*
|
|
74
|
+
* They are not interchangeable and the reading side picks by campaign type, so
|
|
75
|
+
* the wrong one produces a file that decrypts cleanly and then parses to
|
|
76
|
+
* nothing:
|
|
77
|
+
*
|
|
78
|
+
* - **`json`** -- `[{waba, custom_fields: [{id, value}]}, …]`. What a bulk
|
|
79
|
+
* campaign's single file holds.
|
|
80
|
+
* - **`table`** -- a `||`-delimited table with a header row. What each bucket of
|
|
81
|
+
* a group campaign holds:
|
|
82
|
+
*
|
|
83
|
+
* ```
|
|
84
|
+
* waba||first_name||amount
|
|
85
|
+
* 919000000001||Priya||1299
|
|
86
|
+
* 919000000002||Ravi||499
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* The table form is narrower than the JSON one: every row has to carry the same
|
|
90
|
+
* columns in the same order, and a value cannot contain a newline -- so newlines
|
|
91
|
+
* are stripped from values rather than escaped, which is what the panel did.
|
|
92
|
+
* Columns are taken from the first receiver, because a header can only be
|
|
93
|
+
* written once.
|
|
94
|
+
*/
|
|
95
|
+
export type ReceiverEncoding = 'json' | 'table';
|
|
96
|
+
/** The `||` table a group campaign's bucket file holds. */
|
|
97
|
+
export declare const encodeReceiverTable: (receivers: readonly CampaignReceiver[]) => string;
|
|
98
|
+
/**
|
|
99
|
+
* Turns a receiver list into the encrypted file that gets uploaded.
|
|
100
|
+
*
|
|
101
|
+
* The filename is cosmetic: the media service stores the object under a uuid and
|
|
102
|
+
* keeps only the extension and the display name. The `im_` prefix and `.txt`
|
|
103
|
+
* extension match what is already in the bucket, so an operator browsing storage
|
|
104
|
+
* sees one consistent kind of object.
|
|
105
|
+
*/
|
|
106
|
+
export declare const buildReceiverFile: (receivers: CampaignReceiver[], encoding?: ReceiverEncoding) => Promise<ReceiverFile>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildReceiverFile = exports.encodeReceiverTable = exports.encryptReceivers = exports.generateReceiverSecret = void 0;
|
|
4
|
+
const bytes_1 = require("./bytes");
|
|
5
|
+
const md5_1 = require("./md5");
|
|
6
|
+
/**
|
|
7
|
+
* The fixed initialisation vector the receiver file format uses.
|
|
8
|
+
*
|
|
9
|
+
* A constant IV is a real weakness -- it means two files encrypted under the same
|
|
10
|
+
* secret have identical leading blocks, so an observer can tell that two
|
|
11
|
+
* campaigns share a prefix of recipients. It is here because **the reading side
|
|
12
|
+
* is fixed**: the campaign manager decrypts with exactly this IV, and a file
|
|
13
|
+
* encrypted any other way is a file nothing can read.
|
|
14
|
+
*
|
|
15
|
+
* What makes it tolerable rather than merely unavoidable is that the secret is
|
|
16
|
+
* fresh per file (see {@link generateReceiverSecret}), so the IV is never reused
|
|
17
|
+
* under the same key -- which is the specific attack a static IV usually enables.
|
|
18
|
+
*
|
|
19
|
+
* If the format is ever revised, the fix is a random IV prepended to the
|
|
20
|
+
* ciphertext, and it has to land on the manager before it lands here.
|
|
21
|
+
*/
|
|
22
|
+
const FIXED_IV = '678025308de70905';
|
|
23
|
+
/**
|
|
24
|
+
* A fresh secret for one receiver file.
|
|
25
|
+
*
|
|
26
|
+
* Sixteen random bytes as hex -- thirty-two characters, which is the shape
|
|
27
|
+
* already in the field and what the `hash` field on a stored campaign holds.
|
|
28
|
+
*
|
|
29
|
+
* The legacy frontend generated this as 16 random bytes, hex-encoded, then
|
|
30
|
+
* applied `.toString('base64url').replace(/_/gi,'U').replace(/-/gi,'D')`. That
|
|
31
|
+
* chain is a **no-op**: `String.prototype.toString` ignores its argument and
|
|
32
|
+
* returns the string unchanged, and the hex alphabet contains no `_` or `-` for
|
|
33
|
+
* the replacements to find. So the value that reaches the manager has always been
|
|
34
|
+
* plain hex, and this produces the identical thing without the dead code.
|
|
35
|
+
*
|
|
36
|
+
* One secret per file, never reused. That is what keeps the fixed IV above from
|
|
37
|
+
* being the flaw it would otherwise be.
|
|
38
|
+
*/
|
|
39
|
+
const generateReceiverSecret = () => (0, bytes_1.toHex)((0, bytes_1.randomBytes)(16));
|
|
40
|
+
exports.generateReceiverSecret = generateReceiverSecret;
|
|
41
|
+
/**
|
|
42
|
+
* Encrypts the receiver list exactly as the campaign manager expects to read it.
|
|
43
|
+
*
|
|
44
|
+
* ```
|
|
45
|
+
* key = md5(secret) -- 32 lower-case hex characters
|
|
46
|
+
* key bytes = the ASCII bytes of that hex -- 32 bytes, so AES-256
|
|
47
|
+
* iv = ASCII bytes of '678025308de70905'
|
|
48
|
+
* ciphertext = AES-256-CBC, PKCS#7 padded
|
|
49
|
+
* file body = base64(ciphertext)
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* Two parts of that are counter-intuitive and both are deliberate, because they
|
|
53
|
+
* mirror what `CryptoJS.AES.encrypt(text, keyWordArray, {iv})` does:
|
|
54
|
+
*
|
|
55
|
+
* - **The key is the hex *text*, not the digest bytes.** `md5()` returns a
|
|
56
|
+
* 32-character string and it is parsed as UTF-8, which lands on 32 bytes and
|
|
57
|
+
* therefore AES-256. Using the 16 raw digest bytes instead would be AES-128 and
|
|
58
|
+
* would not decrypt.
|
|
59
|
+
* - **There is no `Salted__` header.** CryptoJS only writes one when it derives a
|
|
60
|
+
* key from a passphrase. Given a key directly, as here, its default formatter
|
|
61
|
+
* emits bare base64 of the ciphertext.
|
|
62
|
+
*
|
|
63
|
+
* Web Crypto applies PKCS#7 padding to AES-CBC itself, which is the same padding
|
|
64
|
+
* CryptoJS defaults to -- so the outputs are byte-identical.
|
|
65
|
+
*/
|
|
66
|
+
const encryptReceivers = async (plaintext, secret) => {
|
|
67
|
+
const crypto = (0, bytes_1.subtle)();
|
|
68
|
+
const key = await crypto.importKey('raw', (0, bytes_1.toArrayBuffer)((0, bytes_1.utf8)((0, md5_1.md5)(secret))), { name: 'AES-CBC' }, false, [
|
|
69
|
+
'encrypt',
|
|
70
|
+
]);
|
|
71
|
+
const ciphertext = await crypto.encrypt({ name: 'AES-CBC', iv: (0, bytes_1.toArrayBuffer)((0, bytes_1.utf8)(FIXED_IV)) }, key, (0, bytes_1.toArrayBuffer)((0, bytes_1.utf8)(plaintext)));
|
|
72
|
+
return (0, bytes_1.toBase64)(new Uint8Array(ciphertext));
|
|
73
|
+
};
|
|
74
|
+
exports.encryptReceivers = encryptReceivers;
|
|
75
|
+
const COLUMN_SEPARATOR = '||';
|
|
76
|
+
/** The `||` table a group campaign's bucket file holds. */
|
|
77
|
+
const encodeReceiverTable = (receivers) => {
|
|
78
|
+
const first = receivers[0];
|
|
79
|
+
if (!first)
|
|
80
|
+
return '';
|
|
81
|
+
const columns = first.custom_fields.map((field) => field.id);
|
|
82
|
+
const header = ['waba', ...columns].join(COLUMN_SEPARATOR);
|
|
83
|
+
const rows = receivers.map((receiver) => {
|
|
84
|
+
// Addressed by column rather than by position: a row whose variables arrived
|
|
85
|
+
// in a different order would otherwise silently shift every value one place.
|
|
86
|
+
const byId = new Map(receiver.custom_fields.map((field) => [field.id, field.value]));
|
|
87
|
+
return [receiver.waba, ...columns.map((column) => flatten(byId.get(column) ?? ''))].join(COLUMN_SEPARATOR);
|
|
88
|
+
});
|
|
89
|
+
// Trailing newline: the panel's writer ended every row with one, so the last
|
|
90
|
+
// line is terminated rather than merely ending.
|
|
91
|
+
return `${[header, ...rows].join('\n')}\n`;
|
|
92
|
+
};
|
|
93
|
+
exports.encodeReceiverTable = encodeReceiverTable;
|
|
94
|
+
/**
|
|
95
|
+
* A value as a table cell: no newlines, no separator.
|
|
96
|
+
*
|
|
97
|
+
* Newlines are stripped because the format has no escape for them -- one would
|
|
98
|
+
* end the row early and shift every later value into the wrong column. The
|
|
99
|
+
* separator is stripped for the same reason. Both are replaced by a space rather
|
|
100
|
+
* than removed, so two words do not run together.
|
|
101
|
+
*/
|
|
102
|
+
const flatten = (value) => value.replace(/[\r\n]+/g, ' ').split(COLUMN_SEPARATOR).join(' ');
|
|
103
|
+
/**
|
|
104
|
+
* Turns a receiver list into the encrypted file that gets uploaded.
|
|
105
|
+
*
|
|
106
|
+
* The filename is cosmetic: the media service stores the object under a uuid and
|
|
107
|
+
* keeps only the extension and the display name. The `im_` prefix and `.txt`
|
|
108
|
+
* extension match what is already in the bucket, so an operator browsing storage
|
|
109
|
+
* sees one consistent kind of object.
|
|
110
|
+
*/
|
|
111
|
+
const buildReceiverFile = async (receivers, encoding = 'json') => {
|
|
112
|
+
const secret = (0, exports.generateReceiverSecret)();
|
|
113
|
+
const plaintext = encoding === 'table' ? (0, exports.encodeReceiverTable)(receivers) : JSON.stringify(receivers);
|
|
114
|
+
const body = await (0, exports.encryptReceivers)(plaintext, secret);
|
|
115
|
+
return {
|
|
116
|
+
body,
|
|
117
|
+
secret,
|
|
118
|
+
filename: `im_${Date.now().toString(36)}${(0, bytes_1.toHex)((0, bytes_1.randomBytes)(4))}.txt`,
|
|
119
|
+
contentType: 'text/plain',
|
|
120
|
+
// The ciphertext is base64, so every character is one byte. Measuring the
|
|
121
|
+
// string rather than re-encoding it is exact here, and the upload signature
|
|
122
|
+
// covers this number.
|
|
123
|
+
size: body.length,
|
|
124
|
+
receivers: receivers.length,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
exports.buildReceiverFile = buildReceiverFile;
|
|
128
|
+
//# sourceMappingURL=receiver-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"receiver-file.js","sourceRoot":"","sources":["../../src/internal/receiver-file.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type Socket } from 'socket.io-client';
|
|
2
|
+
import type { InboxPushName, InboxPushes, InboxRequestName, InboxRequests } from './events';
|
|
3
|
+
import type { MediaUploader } from '../types/inbox';
|
|
4
|
+
export interface InboxClientOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Where the inbox socket listens. Its own origin, not the REST gateway's: the
|
|
7
|
+
* gateway proxies HTTP only and forwards no upgrade.
|
|
8
|
+
*/
|
|
9
|
+
url: string;
|
|
10
|
+
/**
|
|
11
|
+
* Ecosystem bearer token, or a function returning one. A function is called on
|
|
12
|
+
* every connect, so a reconnect after an expiry picks up a fresh token instead
|
|
13
|
+
* of retrying with the one that just failed.
|
|
14
|
+
*/
|
|
15
|
+
token: string | (() => string | Promise<string>);
|
|
16
|
+
/** Your ecosystem client id. Sent on the handshake, as on every REST call. */
|
|
17
|
+
clientId: string;
|
|
18
|
+
/** Socket.IO path, if the deployment moved it. */
|
|
19
|
+
path?: string;
|
|
20
|
+
/** How long a request waits for its acknowledgement. Default 20s. */
|
|
21
|
+
timeout?: number;
|
|
22
|
+
/** Connect on construction. Default `true`. */
|
|
23
|
+
autoConnect?: boolean;
|
|
24
|
+
/** Extra socket.io options, merged last. */
|
|
25
|
+
socketOptions?: Record<string, unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* How to upload a file, for `messages.sendMedia({ file })`.
|
|
28
|
+
*
|
|
29
|
+
* A socket cannot presign -- the ticket comes from the REST API -- so this is
|
|
30
|
+
* the seam between the two. Satisfied by
|
|
31
|
+
* `crm.inboxMedia.upload.bind(crm.inboxMedia)`.
|
|
32
|
+
*
|
|
33
|
+
* Optional, and a function rather than a client, so an app that only ever sends
|
|
34
|
+
* an already-uploaded `link` pulls in none of the REST surface.
|
|
35
|
+
*/
|
|
36
|
+
uploadMedia?: MediaUploader;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The live-chat inbox over a socket.
|
|
40
|
+
*
|
|
41
|
+
* Requests are promises: each emit carries an acknowledgement callback, so two
|
|
42
|
+
* concurrent reads of different threads cannot be confused for one another. The
|
|
43
|
+
* server also emits replies under the request's own event name for older clients
|
|
44
|
+
* -- {@link InboxClient.on} sees those too, which is why a handler for
|
|
45
|
+
* `chat.action` receives both your own actions and other agents'.
|
|
46
|
+
*
|
|
47
|
+
* Credentials travel in `auth`, not in a header: a browser cannot set headers on
|
|
48
|
+
* a WebSocket upgrade, so a header-based handshake only works for as long as the
|
|
49
|
+
* connection stays on long-polling.
|
|
50
|
+
*/
|
|
51
|
+
export declare class InboxClient {
|
|
52
|
+
private readonly options;
|
|
53
|
+
private readonly socket;
|
|
54
|
+
private readonly timeout;
|
|
55
|
+
/**
|
|
56
|
+
* The uploader this client was built with, if any.
|
|
57
|
+
*
|
|
58
|
+
* Read by `messages.sendMedia` when it is handed a file rather than a link.
|
|
59
|
+
*/
|
|
60
|
+
get uploadMedia(): MediaUploader | undefined;
|
|
61
|
+
constructor(options: InboxClientOptions);
|
|
62
|
+
/** Resolved per connect, so a reconnect never reuses an expired token. */
|
|
63
|
+
private credentials;
|
|
64
|
+
/** The underlying socket, for lifecycle events and anything not wrapped here. */
|
|
65
|
+
get raw(): Socket;
|
|
66
|
+
get connected(): boolean;
|
|
67
|
+
connect(): this;
|
|
68
|
+
disconnect(): this;
|
|
69
|
+
/** Resolves once connected, or rejects with the handshake's reason. */
|
|
70
|
+
ready(): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Sends one request and waits for its answer.
|
|
73
|
+
*
|
|
74
|
+
* A refusal rejects with a {@link CrmApiError} carrying the same code and status
|
|
75
|
+
* the REST API would have returned, so one error path covers both transports.
|
|
76
|
+
*/
|
|
77
|
+
request<E extends InboxRequestName>(event: E, payload: InboxRequests[E]['payload']): Promise<InboxRequests[E]['result']>;
|
|
78
|
+
/**
|
|
79
|
+
* Listens for a pushed event. Returns the unsubscribe function, so a component
|
|
80
|
+
* can tear its own listener down without knowing what else is attached.
|
|
81
|
+
*/
|
|
82
|
+
on<E extends InboxPushName>(event: E, handler: (payload: InboxPushes[E]) => void): () => void;
|
|
83
|
+
/** Fires once, then unsubscribes. */
|
|
84
|
+
once<E extends InboxPushName>(event: E, handler: (payload: InboxPushes[E]) => void): void;
|
|
85
|
+
}
|