@hellotext/hellotext 2.3.6 → 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/index.d.ts CHANGED
@@ -70,7 +70,8 @@ declare class Hellotext {
70
70
  export declare class User {
71
71
  static get id(): string | undefined
72
72
  static get source(): string | undefined
73
- static remember(id: string, source?: string): void
73
+ static get fingerprint(): string | undefined
74
+ static remember(id: string, source?: string, fingerprint?: string): void
74
75
  static forget(): void
75
76
  static get identificationData(): IdentificationData | Record<string, never>
76
77
  }
package/lib/hellotext.cjs CHANGED
@@ -85,7 +85,9 @@ let Hellotext = /*#__PURE__*/function () {
85
85
  * @property { String } [name] - the name of the user
86
86
  * @property { String } [source] - the platform specific identifier where this pixel is running on.
87
87
  *
88
- * Identifies a user and attaches the hello_session to the user ID
88
+ * Identifies a user and attaches the hello_session to the user ID.
89
+ * Repeated calls are skipped only when the last successful identify payload
90
+ * for the current session remains unchanged.
89
91
  * @param { String } externalId - the user ID
90
92
  * @param { IdentificationOptions } options - the options for the identification
91
93
  * @returns {Promise<Response>}
@@ -93,11 +95,12 @@ let Hellotext = /*#__PURE__*/function () {
93
95
  }, {
94
96
  key: "identify",
95
97
  value: async function identify(externalId, options = {}) {
96
- if (_models.User.id === externalId) {
98
+ const fingerprint = await _models.Fingerprint.generate(this.session, externalId, options);
99
+ if (_models.Fingerprint.matches(_models.User.fingerprint, fingerprint)) {
97
100
  return new _api.Response(true, {
98
- json: async () => {
99
- already_identified: true;
100
- }
101
+ json: async () => ({
102
+ already_identified: true
103
+ })
101
104
  });
102
105
  }
103
106
  const response = await _api.default.identifications.create({
@@ -105,7 +108,7 @@ let Hellotext = /*#__PURE__*/function () {
105
108
  ...options
106
109
  });
107
110
  if (response.succeeded) {
108
- _models.User.remember(externalId, options.source);
111
+ _models.User.remember(externalId, options.source, fingerprint);
109
112
  }
110
113
  return response;
111
114
  }
package/lib/hellotext.js CHANGED
@@ -10,7 +10,7 @@ function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typ
10
10
  function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
11
11
  import { Configuration, Event } from './core';
12
12
  import API, { Response } from './api';
13
- import { Business, FormCollection, Page, Query, Session, User, Webchat } from './models';
13
+ import { Business, Fingerprint, FormCollection, Page, Query, Session, User, Webchat } from './models';
14
14
  import { NotInitializedError } from './errors';
15
15
  var Hellotext = /*#__PURE__*/function () {
16
16
  function Hellotext() {
@@ -85,7 +85,9 @@ var Hellotext = /*#__PURE__*/function () {
85
85
  * @property { String } [name] - the name of the user
86
86
  * @property { String } [source] - the platform specific identifier where this pixel is running on.
87
87
  *
88
- * Identifies a user and attaches the hello_session to the user ID
88
+ * Identifies a user and attaches the hello_session to the user ID.
89
+ * Repeated calls are skipped only when the last successful identify payload
90
+ * for the current session remains unchanged.
89
91
  * @param { String } externalId - the user ID
90
92
  * @param { IdentificationOptions } options - the options for the identification
91
93
  * @returns {Promise<Response>}
@@ -95,11 +97,14 @@ var Hellotext = /*#__PURE__*/function () {
95
97
  value: function () {
96
98
  var _identify = _asyncToGenerator(function* (externalId) {
97
99
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
98
- if (User.id === externalId) {
100
+ var fingerprint = yield Fingerprint.generate(this.session, externalId, options);
101
+ if (Fingerprint.matches(User.fingerprint, fingerprint)) {
99
102
  return new Response(true, {
100
103
  json: function () {
101
104
  var _json = _asyncToGenerator(function* () {
102
- already_identified: true;
105
+ return {
106
+ already_identified: true
107
+ };
103
108
  });
104
109
  function json() {
105
110
  return _json.apply(this, arguments);
@@ -112,7 +117,7 @@ var Hellotext = /*#__PURE__*/function () {
112
117
  user_id: externalId
113
118
  }, options));
114
119
  if (response.succeeded) {
115
- User.remember(externalId, options.source);
120
+ User.remember(externalId, options.source, fingerprint);
116
121
  }
117
122
  return response;
118
123
  });
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.Fingerprint = void 0;
7
+ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
8
+ function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
9
+ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
10
+ function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
11
+ function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
12
+ function normalizeValue(value) {
13
+ // Collapse "missing" values so callers can add optional fields incrementally
14
+ // without changing the fingerprint when the effective payload is the same.
15
+ if (value === null || value === undefined) {
16
+ return undefined;
17
+ }
18
+ if (typeof value === 'string') {
19
+ const trimmedValue = value.trim();
20
+
21
+ // Treat blank strings as absent values so "" and an omitted field compare equally.
22
+ return trimmedValue === '' ? undefined : trimmedValue;
23
+ }
24
+ if (Array.isArray(value)) {
25
+ // Preserve array order because, unlike object keys, caller-provided sequence can be meaningful.
26
+ return value.map(item => normalizeValue(item)).filter(item => item !== undefined);
27
+ }
28
+ if (value instanceof Date) {
29
+ // Serialize dates into a stable primitive so equivalent timestamps fingerprint the same way.
30
+ return value.toISOString();
31
+ }
32
+ if (typeof value === 'object') {
33
+ // Canonicalize object shape by sorting keys recursively, so key placement never affects equality.
34
+ const normalizedObject = Object.keys(value).sort((leftKey, rightKey) => leftKey.localeCompare(rightKey)).reduce((result, key) => {
35
+ const normalizedChild = normalizeValue(value[key]);
36
+ if (normalizedChild !== undefined) {
37
+ result[key] = normalizedChild;
38
+ }
39
+ return result;
40
+ }, {});
41
+ return Object.keys(normalizedObject).length > 0 ? normalizedObject : undefined;
42
+ }
43
+ if (typeof value === 'number' || typeof value === 'boolean') {
44
+ return value;
45
+ }
46
+ return undefined;
47
+ }
48
+ function serializePayload(session, userId, options = {}) {
49
+ const normalizedPayload = normalizeValue({
50
+ session,
51
+ user_id: userId,
52
+ ...options
53
+ }) || {};
54
+ return JSON.stringify(normalizedPayload);
55
+ }
56
+ function fallbackHash(value) {
57
+ let hash = 5381;
58
+ for (let index = 0; index < value.length; index += 1) {
59
+ hash = hash * 33 ^ value.charCodeAt(index);
60
+ }
61
+ return `v1:${(hash >>> 0).toString(16)}`;
62
+ }
63
+ async function sha256(value) {
64
+ var _globalThis$crypto;
65
+ if (!((_globalThis$crypto = globalThis.crypto) !== null && _globalThis$crypto !== void 0 && _globalThis$crypto.subtle) || typeof TextEncoder === 'undefined') {
66
+ return fallbackHash(value);
67
+ }
68
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
69
+ const hex = Array.from(new Uint8Array(digest)).map(byte => byte.toString(16).padStart(2, '0')).join('');
70
+ return `v1:${hex}`;
71
+ }
72
+ let Fingerprint = /*#__PURE__*/function () {
73
+ function Fingerprint() {
74
+ _classCallCheck(this, Fingerprint);
75
+ }
76
+ _createClass(Fingerprint, null, [{
77
+ key: "matches",
78
+ value: function matches(storedFingerprint, fingerprint) {
79
+ return !!storedFingerprint && storedFingerprint === fingerprint;
80
+ }
81
+ }, {
82
+ key: "generate",
83
+ value: async function generate(session, userId, options = {}) {
84
+ return await sha256(serializePayload(session, userId, options));
85
+ }
86
+ }]);
87
+ return Fingerprint;
88
+ }();
89
+ exports.Fingerprint = Fingerprint;
@@ -0,0 +1,101 @@
1
+ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
2
+ function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
3
+ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
4
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
5
+ function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
6
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
7
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
8
+ function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
9
+ function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
10
+ function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
11
+ function normalizeValue(value) {
12
+ // Collapse "missing" values so callers can add optional fields incrementally
13
+ // without changing the fingerprint when the effective payload is the same.
14
+ if (value === null || value === undefined) {
15
+ return undefined;
16
+ }
17
+ if (typeof value === 'string') {
18
+ var trimmedValue = value.trim();
19
+
20
+ // Treat blank strings as absent values so "" and an omitted field compare equally.
21
+ return trimmedValue === '' ? undefined : trimmedValue;
22
+ }
23
+ if (Array.isArray(value)) {
24
+ // Preserve array order because, unlike object keys, caller-provided sequence can be meaningful.
25
+ return value.map(item => normalizeValue(item)).filter(item => item !== undefined);
26
+ }
27
+ if (value instanceof Date) {
28
+ // Serialize dates into a stable primitive so equivalent timestamps fingerprint the same way.
29
+ return value.toISOString();
30
+ }
31
+ if (typeof value === 'object') {
32
+ // Canonicalize object shape by sorting keys recursively, so key placement never affects equality.
33
+ var normalizedObject = Object.keys(value).sort((leftKey, rightKey) => leftKey.localeCompare(rightKey)).reduce((result, key) => {
34
+ var normalizedChild = normalizeValue(value[key]);
35
+ if (normalizedChild !== undefined) {
36
+ result[key] = normalizedChild;
37
+ }
38
+ return result;
39
+ }, {});
40
+ return Object.keys(normalizedObject).length > 0 ? normalizedObject : undefined;
41
+ }
42
+ if (typeof value === 'number' || typeof value === 'boolean') {
43
+ return value;
44
+ }
45
+ return undefined;
46
+ }
47
+ function serializePayload(session, userId) {
48
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
49
+ var normalizedPayload = normalizeValue(_objectSpread({
50
+ session,
51
+ user_id: userId
52
+ }, options)) || {};
53
+ return JSON.stringify(normalizedPayload);
54
+ }
55
+ function fallbackHash(value) {
56
+ var hash = 5381;
57
+ for (var index = 0; index < value.length; index += 1) {
58
+ hash = hash * 33 ^ value.charCodeAt(index);
59
+ }
60
+ return "v1:".concat((hash >>> 0).toString(16));
61
+ }
62
+ function sha256(_x) {
63
+ return _sha.apply(this, arguments);
64
+ }
65
+ function _sha() {
66
+ _sha = _asyncToGenerator(function* (value) {
67
+ var _globalThis$crypto;
68
+ if (!((_globalThis$crypto = globalThis.crypto) !== null && _globalThis$crypto !== void 0 && _globalThis$crypto.subtle) || typeof TextEncoder === 'undefined') {
69
+ return fallbackHash(value);
70
+ }
71
+ var digest = yield globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
72
+ var hex = Array.from(new Uint8Array(digest)).map(byte => byte.toString(16).padStart(2, '0')).join('');
73
+ return "v1:".concat(hex);
74
+ });
75
+ return _sha.apply(this, arguments);
76
+ }
77
+ var Fingerprint = /*#__PURE__*/function () {
78
+ function Fingerprint() {
79
+ _classCallCheck(this, Fingerprint);
80
+ }
81
+ _createClass(Fingerprint, null, [{
82
+ key: "matches",
83
+ value: function matches(storedFingerprint, fingerprint) {
84
+ return !!storedFingerprint && storedFingerprint === fingerprint;
85
+ }
86
+ }, {
87
+ key: "generate",
88
+ value: function () {
89
+ var _generate = _asyncToGenerator(function* (session, userId) {
90
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
91
+ return yield sha256(serializePayload(session, userId, options));
92
+ });
93
+ function generate(_x2, _x3) {
94
+ return _generate.apply(this, arguments);
95
+ }
96
+ return generate;
97
+ }()
98
+ }]);
99
+ return Fingerprint;
100
+ }();
101
+ export { Fingerprint };
@@ -15,6 +15,12 @@ Object.defineProperty(exports, "Cookies", {
15
15
  return _cookies.Cookies;
16
16
  }
17
17
  });
18
+ Object.defineProperty(exports, "Fingerprint", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _fingerprint.Fingerprint;
22
+ }
23
+ });
18
24
  Object.defineProperty(exports, "Form", {
19
25
  enumerable: true,
20
26
  get: function () {
@@ -65,6 +71,7 @@ Object.defineProperty(exports, "Webchat", {
65
71
  });
66
72
  var _business = require("./business");
67
73
  var _cookies = require("./cookies");
74
+ var _fingerprint = require("./fingerprint");
68
75
  var _form = require("./form");
69
76
  var _form_collection = require("./form_collection");
70
77
  var _page = require("./page");
@@ -1,5 +1,6 @@
1
1
  export { Business } from './business';
2
2
  export { Cookies } from './cookies';
3
+ export { Fingerprint } from './fingerprint';
3
4
  export { Form } from './form';
4
5
  export { FormCollection } from './form_collection';
5
6
  export { Page } from './page';
@@ -24,12 +24,20 @@ let User = /*#__PURE__*/function () {
24
24
  get: function () {
25
25
  return _cookies.Cookies.get('hello_user_source');
26
26
  }
27
+ }, {
28
+ key: "fingerprint",
29
+ get: function () {
30
+ return _cookies.Cookies.get('hello_user_identification_hash');
31
+ }
27
32
  }, {
28
33
  key: "remember",
29
- value: function remember(id, source) {
34
+ value: function remember(id, source, fingerprint) {
30
35
  if (source) {
31
36
  _cookies.Cookies.set('hello_user_source', source);
32
37
  }
38
+ if (fingerprint) {
39
+ _cookies.Cookies.set('hello_user_identification_hash', fingerprint);
40
+ }
33
41
  _cookies.Cookies.set('hello_user_id', id);
34
42
  }
35
43
  }, {
@@ -37,6 +45,7 @@ let User = /*#__PURE__*/function () {
37
45
  value: function forget() {
38
46
  _cookies.Cookies.delete('hello_user_id');
39
47
  _cookies.Cookies.delete('hello_user_source');
48
+ _cookies.Cookies.delete('hello_user_identification_hash');
40
49
  }
41
50
  }, {
42
51
  key: "identificationData",
@@ -18,12 +18,20 @@ var User = /*#__PURE__*/function () {
18
18
  get: function get() {
19
19
  return Cookies.get('hello_user_source');
20
20
  }
21
+ }, {
22
+ key: "fingerprint",
23
+ get: function get() {
24
+ return Cookies.get('hello_user_identification_hash');
25
+ }
21
26
  }, {
22
27
  key: "remember",
23
- value: function remember(id, source) {
28
+ value: function remember(id, source, fingerprint) {
24
29
  if (source) {
25
30
  Cookies.set('hello_user_source', source);
26
31
  }
32
+ if (fingerprint) {
33
+ Cookies.set('hello_user_identification_hash', fingerprint);
34
+ }
27
35
  Cookies.set('hello_user_id', id);
28
36
  }
29
37
  }, {
@@ -31,6 +39,7 @@ var User = /*#__PURE__*/function () {
31
39
  value: function forget() {
32
40
  Cookies.delete('hello_user_id');
33
41
  Cookies.delete('hello_user_source');
42
+ Cookies.delete('hello_user_identification_hash');
34
43
  }
35
44
  }, {
36
45
  key: "identificationData",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellotext/hellotext",
3
- "version": "2.3.6",
3
+ "version": "2.3.7",
4
4
  "description": "Hellotext JavaScript Client",
5
5
  "source": "src/index.js",
6
6
  "main": "lib/index.cjs",
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
 
@@ -83,17 +83,21 @@ class Hellotext {
83
83
  * @property { String } [name] - the name of the user
84
84
  * @property { String } [source] - the platform specific identifier where this pixel is running on.
85
85
  *
86
- * 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.
87
89
  * @param { String } externalId - the user ID
88
90
  * @param { IdentificationOptions } options - the options for the identification
89
91
  * @returns {Promise<Response>}
90
92
  */
91
93
  static async identify(externalId, options = {}) {
92
- if (User.id === externalId) {
94
+ const fingerprint = await Fingerprint.generate(this.session, externalId, options)
95
+
96
+ if (Fingerprint.matches(User.fingerprint, fingerprint)) {
93
97
  return new Response(true, {
94
- json: async () => {
95
- already_identified: true
96
- },
98
+ json: async () => ({
99
+ already_identified: true,
100
+ }),
97
101
  })
98
102
  }
99
103
 
@@ -103,7 +107,11 @@ class Hellotext {
103
107
  })
104
108
 
105
109
  if (response.succeeded) {
106
- User.remember(externalId, options.source)
110
+ User.remember(
111
+ externalId,
112
+ options.source,
113
+ fingerprint,
114
+ )
107
115
  }
108
116
 
109
117
  return response
@@ -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 }
@@ -1,5 +1,6 @@
1
1
  export { Business } from './business'
2
2
  export { Cookies } from './cookies'
3
+ export { Fingerprint } from './fingerprint'
3
4
  export { Form } from './form'
4
5
  export { FormCollection } from './form_collection'
5
6
  export { Page } from './page'
@@ -9,17 +9,26 @@ class User {
9
9
  return Cookies.get('hello_user_source')
10
10
  }
11
11
 
12
- static remember(id, source) {
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() {