@huloglobal/vendure-plugin-visitor-analytics 0.17.2 → 0.18.0
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/CHANGELOG.md +13 -0
- package/README.md +70 -2
- package/dist/abandoned-cart.controller.d.ts +70 -14
- package/dist/abandoned-cart.controller.d.ts.map +1 -1
- package/dist/abandoned-cart.controller.js +214 -13
- package/dist/abandoned-cart.controller.js.map +1 -1
- package/dist/abandoned-cart.entity.d.ts +15 -0
- package/dist/abandoned-cart.entity.d.ts.map +1 -1
- package/dist/abandoned-cart.entity.js +20 -0
- package/dist/abandoned-cart.entity.js.map +1 -1
- package/dist/abandoned-cart.service.d.ts +125 -7
- package/dist/abandoned-cart.service.d.ts.map +1 -1
- package/dist/abandoned-cart.service.js +274 -8
- package/dist/abandoned-cart.service.js.map +1 -1
- package/dist/hulo-storefront.d.ts.map +1 -1
- package/dist/hulo-storefront.js +52 -0
- package/dist/hulo-storefront.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/recovery-tokens.d.ts +56 -0
- package/dist/recovery-tokens.d.ts.map +1 -0
- package/dist/recovery-tokens.js +125 -0
- package/dist/recovery-tokens.js.map +1 -0
- package/dist/recovery-tokens.test.d.ts +2 -0
- package/dist/recovery-tokens.test.d.ts.map +1 -0
- package/dist/recovery-tokens.test.js +115 -0
- package/dist/recovery-tokens.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RESUMABLE_ORDER_STATES = void 0;
|
|
4
|
+
exports.isResumableOrderState = isResumableOrderState;
|
|
5
|
+
exports.recoveryStepRank = recoveryStepRank;
|
|
6
|
+
exports.advanceRecoveryStep = advanceRecoveryStep;
|
|
7
|
+
exports.normaliseEmail = normaliseEmail;
|
|
8
|
+
exports.hashEmail = hashEmail;
|
|
9
|
+
exports.buildOptOutToken = buildOptOutToken;
|
|
10
|
+
exports.verifyOptOutToken = verifyOptOutToken;
|
|
11
|
+
exports.buildOptOutUrl = buildOptOutUrl;
|
|
12
|
+
exports.buildListUnsubscribeHeaders = buildListUnsubscribeHeaders;
|
|
13
|
+
exports.sanitiseOrderCode = sanitiseOrderCode;
|
|
14
|
+
const crypto_1 = require("crypto");
|
|
15
|
+
/**
|
|
16
|
+
* Pure helpers behind the cart-recovery attribution + opt-out features
|
|
17
|
+
* (0.18.0). No I/O — everything here is unit-tested in isolation.
|
|
18
|
+
*/
|
|
19
|
+
/** Vendure order states from which a customer can still pick up the same
|
|
20
|
+
* order (the storefront can add items / arrange payment). Anything later —
|
|
21
|
+
* `PaymentAuthorized`, `PaymentSettled`, `Cancelled`, … — is a finished or
|
|
22
|
+
* dead order and the storefront should rebuild a fresh cart instead. */
|
|
23
|
+
exports.RESUMABLE_ORDER_STATES = ['AddingItems', 'ArrangingPayment'];
|
|
24
|
+
function isResumableOrderState(state) {
|
|
25
|
+
return !!state && exports.RESUMABLE_ORDER_STATES.includes(String(state));
|
|
26
|
+
}
|
|
27
|
+
const RECOVERY_STEP_ORDER = ['link_issued', 'link_opened', 'resumed', 'converted'];
|
|
28
|
+
function recoveryStepRank(step) {
|
|
29
|
+
const i = RECOVERY_STEP_ORDER.indexOf(String(step || ''));
|
|
30
|
+
return i;
|
|
31
|
+
}
|
|
32
|
+
/** Returns the later of `current` and `target`; never regresses. Unknown
|
|
33
|
+
* values in `current` (legacy rows, hand edits) are treated as "no step". */
|
|
34
|
+
function advanceRecoveryStep(current, target) {
|
|
35
|
+
return recoveryStepRank(current) >= recoveryStepRank(target)
|
|
36
|
+
? current
|
|
37
|
+
: target;
|
|
38
|
+
}
|
|
39
|
+
/** Lower-cased, trimmed. Returns '' for anything that is not an email. */
|
|
40
|
+
function normaliseEmail(raw) {
|
|
41
|
+
const s = String(raw || '').trim().toLowerCase();
|
|
42
|
+
if (s.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s))
|
|
43
|
+
return '';
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
/** SHA-256 hex of the normalised email — the same hash `abandoned_cart.emailHash`
|
|
47
|
+
* stores, so opt-outs can be matched without keeping the raw address. */
|
|
48
|
+
function hashEmail(email) {
|
|
49
|
+
return (0, crypto_1.createHash)('sha256').update(normaliseEmail(email)).digest('hex');
|
|
50
|
+
}
|
|
51
|
+
function b64url(buf) {
|
|
52
|
+
return buf.toString('base64url');
|
|
53
|
+
}
|
|
54
|
+
function optOutMac(email, secret) {
|
|
55
|
+
return b64url((0, crypto_1.createHmac)('sha256', secret).update(`opt-out:${email}`).digest());
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Opt-out token: `<base64url(email)>.<base64url(hmac-sha256(secret, email))>`.
|
|
59
|
+
* Self-describing so the endpoint needs no DB lookup to know *who* is opting
|
|
60
|
+
* out, and unforgeable so a third party cannot unsubscribe someone else.
|
|
61
|
+
* Returns null when there is no secret (feature disabled) or the email is
|
|
62
|
+
* malformed.
|
|
63
|
+
*/
|
|
64
|
+
function buildOptOutToken(email, secret) {
|
|
65
|
+
const e = normaliseEmail(email);
|
|
66
|
+
if (!e || !secret)
|
|
67
|
+
return null;
|
|
68
|
+
return `${b64url(Buffer.from(e, 'utf8'))}.${optOutMac(e, secret)}`;
|
|
69
|
+
}
|
|
70
|
+
/** Inverse of `buildOptOutToken`. Returns the normalised email on success,
|
|
71
|
+
* null on any failure (bad shape, bad MAC, wrong secret). Constant-time
|
|
72
|
+
* MAC comparison. */
|
|
73
|
+
function verifyOptOutToken(token, secret) {
|
|
74
|
+
if (!secret)
|
|
75
|
+
return null;
|
|
76
|
+
const t = String(token || '').trim();
|
|
77
|
+
const dot = t.indexOf('.');
|
|
78
|
+
if (dot <= 0 || dot === t.length - 1)
|
|
79
|
+
return null;
|
|
80
|
+
let email = '';
|
|
81
|
+
try {
|
|
82
|
+
email = normaliseEmail(Buffer.from(t.slice(0, dot), 'base64url').toString('utf8'));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (!email)
|
|
88
|
+
return null;
|
|
89
|
+
const expected = Buffer.from(optOutMac(email, secret));
|
|
90
|
+
const given = Buffer.from(t.slice(dot + 1));
|
|
91
|
+
if (expected.length !== given.length)
|
|
92
|
+
return null;
|
|
93
|
+
return (0, crypto_1.timingSafeEqual)(expected, given) ? email : null;
|
|
94
|
+
}
|
|
95
|
+
/** Build the absolute opt-out URL for an email. `backendBaseUrl` is the
|
|
96
|
+
* Vendure server's public origin (the endpoint lives on the API, not the
|
|
97
|
+
* storefront). */
|
|
98
|
+
function buildOptOutUrl(backendBaseUrl, email, secret) {
|
|
99
|
+
const token = buildOptOutToken(email, secret);
|
|
100
|
+
if (!token)
|
|
101
|
+
return null;
|
|
102
|
+
return `${String(backendBaseUrl || '').replace(/\/$/, '')}/ees/abandoned-carts/opt-out?e=${encodeURIComponent(token)}`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Headers a mailer should add to every abandoned-cart email so Gmail /
|
|
106
|
+
* Outlook / Yahoo render their native "Unsubscribe" affordance
|
|
107
|
+
* (RFC 2369 + RFC 8058 one-click). Returns null when opt-out links are
|
|
108
|
+
* disabled.
|
|
109
|
+
*/
|
|
110
|
+
function buildListUnsubscribeHeaders(backendBaseUrl, email, secret) {
|
|
111
|
+
const url = buildOptOutUrl(backendBaseUrl, email, secret);
|
|
112
|
+
if (!url)
|
|
113
|
+
return null;
|
|
114
|
+
return {
|
|
115
|
+
'List-Unsubscribe': `<${url}>`,
|
|
116
|
+
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Order codes are Vendure-generated (16 upper-case alphanumerics by default)
|
|
120
|
+
* but hosts can override the generator, so accept a generous safe subset. */
|
|
121
|
+
function sanitiseOrderCode(raw) {
|
|
122
|
+
const s = String(raw || '').trim();
|
|
123
|
+
return /^[A-Za-z0-9_-]{4,64}$/.test(s) ? s : '';
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=recovery-tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recovery-tokens.js","sourceRoot":"","sources":["../src/recovery-tokens.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recovery-tokens.test.d.ts","sourceRoot":"","sources":["../src/recovery-tokens.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const recovery_tokens_1 = require("./recovery-tokens");
|
|
5
|
+
const SECRET = 'unit-test-secret-do-not-use';
|
|
6
|
+
(0, vitest_1.describe)('isResumableOrderState', () => {
|
|
7
|
+
(0, vitest_1.it)('accepts the two open states only', () => {
|
|
8
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.isResumableOrderState)('AddingItems')).toBe(true);
|
|
9
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.isResumableOrderState)('ArrangingPayment')).toBe(true);
|
|
10
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.isResumableOrderState)('PaymentAuthorized')).toBe(false);
|
|
11
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.isResumableOrderState)('PaymentSettled')).toBe(false);
|
|
12
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.isResumableOrderState)('Cancelled')).toBe(false);
|
|
13
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.isResumableOrderState)('')).toBe(false);
|
|
14
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.isResumableOrderState)(null)).toBe(false);
|
|
15
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.isResumableOrderState)(undefined)).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
(0, vitest_1.describe)('recovery step lifecycle', () => {
|
|
19
|
+
(0, vitest_1.it)('ranks the steps in funnel order', () => {
|
|
20
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.recoveryStepRank)('link_issued')).toBeLessThan((0, recovery_tokens_1.recoveryStepRank)('link_opened'));
|
|
21
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.recoveryStepRank)('link_opened')).toBeLessThan((0, recovery_tokens_1.recoveryStepRank)('resumed'));
|
|
22
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.recoveryStepRank)('resumed')).toBeLessThan((0, recovery_tokens_1.recoveryStepRank)('converted'));
|
|
23
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.recoveryStepRank)(null)).toBe(-1);
|
|
24
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.recoveryStepRank)('garbage')).toBe(-1);
|
|
25
|
+
});
|
|
26
|
+
(0, vitest_1.it)('advances forward', () => {
|
|
27
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.advanceRecoveryStep)(null, 'link_issued')).toBe('link_issued');
|
|
28
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.advanceRecoveryStep)('link_issued', 'link_opened')).toBe('link_opened');
|
|
29
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.advanceRecoveryStep)('link_opened', 'converted')).toBe('converted');
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.it)('never regresses', () => {
|
|
32
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.advanceRecoveryStep)('converted', 'link_issued')).toBe('converted');
|
|
33
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.advanceRecoveryStep)('resumed', 'link_opened')).toBe('resumed');
|
|
34
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.advanceRecoveryStep)('link_opened', 'link_opened')).toBe('link_opened');
|
|
35
|
+
});
|
|
36
|
+
(0, vitest_1.it)('treats unknown current values as no step', () => {
|
|
37
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.advanceRecoveryStep)('legacy-value', 'link_opened')).toBe('link_opened');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
(0, vitest_1.describe)('normaliseEmail / hashEmail', () => {
|
|
41
|
+
(0, vitest_1.it)('lower-cases and trims', () => {
|
|
42
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.normaliseEmail)(' Buyer@Example.COM ')).toBe('buyer@example.com');
|
|
43
|
+
});
|
|
44
|
+
(0, vitest_1.it)('rejects non-emails', () => {
|
|
45
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.normaliseEmail)('not an email')).toBe('');
|
|
46
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.normaliseEmail)('')).toBe('');
|
|
47
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.normaliseEmail)(null)).toBe('');
|
|
48
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.normaliseEmail)('a@b')).toBe('');
|
|
49
|
+
});
|
|
50
|
+
(0, vitest_1.it)('hashes the normalised form so case differences collapse', () => {
|
|
51
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.hashEmail)('Buyer@Example.com')).toBe((0, recovery_tokens_1.hashEmail)('buyer@example.com'));
|
|
52
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.hashEmail)('buyer@example.com')).toMatch(/^[0-9a-f]{64}$/);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
(0, vitest_1.describe)('opt-out tokens', () => {
|
|
56
|
+
(0, vitest_1.it)('round-trips', () => {
|
|
57
|
+
const token = (0, recovery_tokens_1.buildOptOutToken)('Buyer@Example.com', SECRET);
|
|
58
|
+
(0, vitest_1.expect)(token).toBeTruthy();
|
|
59
|
+
(0, vitest_1.expect)(token).toMatch(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/);
|
|
60
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.verifyOptOutToken)(token, SECRET)).toBe('buyer@example.com');
|
|
61
|
+
});
|
|
62
|
+
(0, vitest_1.it)('is disabled without a secret', () => {
|
|
63
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.buildOptOutToken)('buyer@example.com', '')).toBeNull();
|
|
64
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.verifyOptOutToken)('anything.anything', '')).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
(0, vitest_1.it)('refuses malformed emails', () => {
|
|
67
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.buildOptOutToken)('nope', SECRET)).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.it)('rejects a tampered email part', () => {
|
|
70
|
+
const token = (0, recovery_tokens_1.buildOptOutToken)('buyer@example.com', SECRET);
|
|
71
|
+
const [, mac] = token.split('.');
|
|
72
|
+
const forged = `${Buffer.from('victim@example.com').toString('base64url')}.${mac}`;
|
|
73
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.verifyOptOutToken)(forged, SECRET)).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.it)('rejects a tampered MAC and a different secret', () => {
|
|
76
|
+
const token = (0, recovery_tokens_1.buildOptOutToken)('buyer@example.com', SECRET);
|
|
77
|
+
const [email, mac] = token.split('.');
|
|
78
|
+
const flipped = mac[0] === 'A' ? 'B' : 'A';
|
|
79
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.verifyOptOutToken)(`${email}.${flipped}${mac.slice(1)}`, SECRET)).toBeNull();
|
|
80
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.verifyOptOutToken)(token, 'other-secret')).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
(0, vitest_1.it)('rejects junk shapes', () => {
|
|
83
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.verifyOptOutToken)('', SECRET)).toBeNull();
|
|
84
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.verifyOptOutToken)('no-dot', SECRET)).toBeNull();
|
|
85
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.verifyOptOutToken)('.mac', SECRET)).toBeNull();
|
|
86
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.verifyOptOutToken)('email.', SECRET)).toBeNull();
|
|
87
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.verifyOptOutToken)(null, SECRET)).toBeNull();
|
|
88
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.verifyOptOutToken)(undefined, SECRET)).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
(0, vitest_1.it)('builds an absolute URL on the backend origin', () => {
|
|
91
|
+
const url = (0, recovery_tokens_1.buildOptOutUrl)('https://api.example.com/', 'buyer@example.com', SECRET);
|
|
92
|
+
(0, vitest_1.expect)(url.startsWith('https://api.example.com/ees/abandoned-carts/opt-out?e=')).toBe(true);
|
|
93
|
+
const e = new URL(url).searchParams.get('e');
|
|
94
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.verifyOptOutToken)(e, SECRET)).toBe('buyer@example.com');
|
|
95
|
+
});
|
|
96
|
+
(0, vitest_1.it)('produces RFC 8058 headers', () => {
|
|
97
|
+
const h = (0, recovery_tokens_1.buildListUnsubscribeHeaders)('https://api.example.com', 'buyer@example.com', SECRET);
|
|
98
|
+
(0, vitest_1.expect)(h['List-Unsubscribe']).toMatch(/^<https:\/\/api\.example\.com\/ees\/abandoned-carts\/opt-out\?e=.+>$/);
|
|
99
|
+
(0, vitest_1.expect)(h['List-Unsubscribe-Post']).toBe('List-Unsubscribe=One-Click');
|
|
100
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.buildListUnsubscribeHeaders)('https://api.example.com', 'buyer@example.com', '')).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
(0, vitest_1.describe)('sanitiseOrderCode', () => {
|
|
104
|
+
(0, vitest_1.it)('accepts Vendure-style codes', () => {
|
|
105
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.sanitiseOrderCode)('S2BZ54TEKQWERTYU')).toBe('S2BZ54TEKQWERTYU');
|
|
106
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.sanitiseOrderCode)(' abc-123_x ')).toBe('abc-123_x');
|
|
107
|
+
});
|
|
108
|
+
(0, vitest_1.it)('rejects anything else', () => {
|
|
109
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.sanitiseOrderCode)('')).toBe('');
|
|
110
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.sanitiseOrderCode)('abc')).toBe('');
|
|
111
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.sanitiseOrderCode)("S2B'; DROP TABLE")).toBe('');
|
|
112
|
+
(0, vitest_1.expect)((0, recovery_tokens_1.sanitiseOrderCode)(null)).toBe('');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
//# sourceMappingURL=recovery-tokens.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recovery-tokens.test.js","sourceRoot":"","sources":["../src/recovery-tokens.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huloglobal/vendure-plugin-visitor-analytics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Full-funnel visitor analytics for Vendure storefronts \u2014 pageviews, time-on-page, session journey, exit pages, funnel drop-off, and per-visitor profile drawer with parsed user-agent + MaxMind GeoLite2 enrichment. Auto-issues visitor + session cookies on first request; logs guest and signed-in events against the same visitor id so the journey survives login.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"author": "Wayne Garrison <wayne@garrison.me.uk>",
|