@hellotext/hellotext 2.3.5 → 2.3.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/dist/hellotext.js +1 -1
- package/index.d.ts +2 -1
- package/lib/api/acks.cjs +42 -0
- package/lib/api/acks.js +42 -0
- package/lib/api/index.cjs +6 -0
- package/lib/api/index.js +6 -0
- package/lib/hellotext.cjs +11 -8
- package/lib/hellotext.js +12 -7
- package/lib/models/fingerprint.cjs +89 -0
- package/lib/models/fingerprint.js +101 -0
- package/lib/models/index.cjs +7 -0
- package/lib/models/index.js +1 -0
- package/lib/models/session.cjs +11 -0
- package/lib/models/session.js +10 -0
- package/lib/models/user.cjs +10 -1
- package/lib/models/user.js +10 -1
- package/package.json +1 -1
- package/src/api/acks.js +25 -0
- package/src/api/index.js +5 -0
- package/src/hellotext.js +18 -9
- package/src/models/fingerprint.js +98 -0
- package/src/models/index.js +1 -0
- package/src/models/session.js +14 -0
- package/src/models/user.js +10 -1
package/src/hellotext.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Configuration, Event } from './core'
|
|
2
2
|
|
|
3
3
|
import API, { Response } from './api'
|
|
4
|
-
import { Business, FormCollection, Page, Query, Session, User, Webchat } from './models'
|
|
4
|
+
import { Business, Fingerprint, FormCollection, Page, Query, Session, User, Webchat } from './models'
|
|
5
5
|
|
|
6
6
|
import { NotInitializedError } from './errors'
|
|
7
7
|
|
|
@@ -17,11 +17,12 @@ class Hellotext {
|
|
|
17
17
|
* @param { Configuration } config
|
|
18
18
|
*/
|
|
19
19
|
static async initialize(business, config) {
|
|
20
|
+
this.business = new Business(business)
|
|
21
|
+
|
|
20
22
|
Configuration.assign(config)
|
|
21
23
|
Session.initialize()
|
|
22
24
|
|
|
23
25
|
this.page = new Page()
|
|
24
|
-
this.business = new Business(business)
|
|
25
26
|
this.forms = new FormCollection()
|
|
26
27
|
|
|
27
28
|
this.query = new Query()
|
|
@@ -82,17 +83,21 @@ class Hellotext {
|
|
|
82
83
|
* @property { String } [name] - the name of the user
|
|
83
84
|
* @property { String } [source] - the platform specific identifier where this pixel is running on.
|
|
84
85
|
*
|
|
85
|
-
* Identifies a user and attaches the hello_session to the user ID
|
|
86
|
+
* Identifies a user and attaches the hello_session to the user ID.
|
|
87
|
+
* Repeated calls are skipped only when the last successful identify payload
|
|
88
|
+
* for the current session remains unchanged.
|
|
86
89
|
* @param { String } externalId - the user ID
|
|
87
90
|
* @param { IdentificationOptions } options - the options for the identification
|
|
88
91
|
* @returns {Promise<Response>}
|
|
89
92
|
*/
|
|
90
93
|
static async identify(externalId, options = {}) {
|
|
91
|
-
|
|
94
|
+
const fingerprint = await Fingerprint.generate(this.session, externalId, options)
|
|
95
|
+
|
|
96
|
+
if (Fingerprint.matches(User.fingerprint, fingerprint)) {
|
|
92
97
|
return new Response(true, {
|
|
93
|
-
json: async () => {
|
|
94
|
-
already_identified: true
|
|
95
|
-
},
|
|
98
|
+
json: async () => ({
|
|
99
|
+
already_identified: true,
|
|
100
|
+
}),
|
|
96
101
|
})
|
|
97
102
|
}
|
|
98
103
|
|
|
@@ -102,7 +107,11 @@ class Hellotext {
|
|
|
102
107
|
})
|
|
103
108
|
|
|
104
109
|
if (response.succeeded) {
|
|
105
|
-
User.remember(
|
|
110
|
+
User.remember(
|
|
111
|
+
externalId,
|
|
112
|
+
options.source,
|
|
113
|
+
fingerprint,
|
|
114
|
+
)
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
return response
|
|
@@ -154,7 +163,7 @@ class Hellotext {
|
|
|
154
163
|
// private
|
|
155
164
|
|
|
156
165
|
static get notInitialized() {
|
|
157
|
-
return this.business.id === undefined
|
|
166
|
+
return !this.business || this.business.id === undefined
|
|
158
167
|
}
|
|
159
168
|
|
|
160
169
|
static get headers() {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
function normalizeValue(value) {
|
|
2
|
+
// Collapse "missing" values so callers can add optional fields incrementally
|
|
3
|
+
// without changing the fingerprint when the effective payload is the same.
|
|
4
|
+
if (value === null || value === undefined) {
|
|
5
|
+
return undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (typeof value === 'string') {
|
|
9
|
+
const trimmedValue = value.trim()
|
|
10
|
+
|
|
11
|
+
// Treat blank strings as absent values so "" and an omitted field compare equally.
|
|
12
|
+
return trimmedValue === '' ? undefined : trimmedValue
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
// Preserve array order because, unlike object keys, caller-provided sequence can be meaningful.
|
|
17
|
+
return value
|
|
18
|
+
.map(item => normalizeValue(item))
|
|
19
|
+
.filter(item => item !== undefined)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (value instanceof Date) {
|
|
23
|
+
// Serialize dates into a stable primitive so equivalent timestamps fingerprint the same way.
|
|
24
|
+
return value.toISOString()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof value === 'object') {
|
|
28
|
+
// Canonicalize object shape by sorting keys recursively, so key placement never affects equality.
|
|
29
|
+
const normalizedObject = Object.keys(value)
|
|
30
|
+
.sort((leftKey, rightKey) => leftKey.localeCompare(rightKey))
|
|
31
|
+
.reduce((result, key) => {
|
|
32
|
+
const normalizedChild = normalizeValue(value[key])
|
|
33
|
+
|
|
34
|
+
if (normalizedChild !== undefined) {
|
|
35
|
+
result[key] = normalizedChild
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result
|
|
39
|
+
}, {})
|
|
40
|
+
|
|
41
|
+
return Object.keys(normalizedObject).length > 0 ? normalizedObject : undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
45
|
+
return value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function serializePayload(session, userId, options = {}) {
|
|
52
|
+
const normalizedPayload = normalizeValue({
|
|
53
|
+
session,
|
|
54
|
+
user_id: userId,
|
|
55
|
+
...options,
|
|
56
|
+
}) || {}
|
|
57
|
+
|
|
58
|
+
return JSON.stringify(normalizedPayload)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fallbackHash(value) {
|
|
62
|
+
let hash = 5381
|
|
63
|
+
|
|
64
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
65
|
+
hash = (hash * 33) ^ value.charCodeAt(index)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return `v1:${(hash >>> 0).toString(16)}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function sha256(value) {
|
|
72
|
+
if (!globalThis.crypto?.subtle || typeof TextEncoder === 'undefined') {
|
|
73
|
+
return fallbackHash(value)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const digest = await globalThis.crypto.subtle.digest(
|
|
77
|
+
'SHA-256',
|
|
78
|
+
new TextEncoder().encode(value),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const hex = Array.from(new Uint8Array(digest))
|
|
82
|
+
.map(byte => byte.toString(16).padStart(2, '0'))
|
|
83
|
+
.join('')
|
|
84
|
+
|
|
85
|
+
return `v1:${hex}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class Fingerprint {
|
|
89
|
+
static matches(storedFingerprint, fingerprint) {
|
|
90
|
+
return !!storedFingerprint && storedFingerprint === fingerprint
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static async generate(session, userId, options = {}) {
|
|
94
|
+
return await sha256(serializePayload(session, userId, options))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { Fingerprint }
|
package/src/models/index.js
CHANGED
package/src/models/session.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Configuration } from '../core'
|
|
2
2
|
import { Cookies } from './cookies'
|
|
3
3
|
import { Query } from './query'
|
|
4
|
+
import API from '../api'
|
|
4
5
|
|
|
5
6
|
class Session {
|
|
6
7
|
static #session
|
|
@@ -11,8 +12,21 @@ class Session {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
static set session(value) {
|
|
15
|
+
const oldSession = Cookies.get('hello_session')
|
|
16
|
+
|
|
14
17
|
this.#session = value
|
|
15
18
|
Cookies.set('hello_session', value)
|
|
19
|
+
|
|
20
|
+
if (oldSession !== value) {
|
|
21
|
+
Cookies.delete('hello_session_ack_at')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!Cookies.get('hello_session_ack_at')) {
|
|
25
|
+
API.acks.send()
|
|
26
|
+
Cookies.set('hello_session_ack_at', new Date().toISOString())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return this.#session
|
|
16
30
|
}
|
|
17
31
|
|
|
18
32
|
static initialize() {
|
package/src/models/user.js
CHANGED
|
@@ -9,17 +9,26 @@ class User {
|
|
|
9
9
|
return Cookies.get('hello_user_source')
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
static
|
|
12
|
+
static get fingerprint() {
|
|
13
|
+
return Cookies.get('hello_user_identification_hash')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static remember(id, source, fingerprint) {
|
|
13
17
|
if (source) {
|
|
14
18
|
Cookies.set('hello_user_source', source)
|
|
15
19
|
}
|
|
16
20
|
|
|
21
|
+
if (fingerprint) {
|
|
22
|
+
Cookies.set('hello_user_identification_hash', fingerprint)
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
Cookies.set('hello_user_id', id)
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
static forget() {
|
|
21
29
|
Cookies.delete('hello_user_id')
|
|
22
30
|
Cookies.delete('hello_user_source')
|
|
31
|
+
Cookies.delete('hello_user_identification_hash')
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
static get identificationData() {
|