@affiliateo/web 4.5.0 → 4.6.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/dist/provider.js +38 -7
- package/dist/turnstile.d.ts +40 -0
- package/dist/turnstile.js +105 -0
- package/package.json +1 -1
package/dist/provider.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createContext, useCallback, useContext, useEffect, useRef, useState } f
|
|
|
4
4
|
import { EventQueue } from './queue';
|
|
5
5
|
import { isLikelyBot } from './bot-detect';
|
|
6
6
|
import { isDevHost } from './dev-host';
|
|
7
|
+
import { mintTurnstileToken, consumeTurnstileToken } from './turnstile';
|
|
7
8
|
const DEFAULT_API_URL = 'https://affiliateo.com';
|
|
8
9
|
const LS_VISITOR_ID = 'affiliateo_visitor_id';
|
|
9
10
|
const LS_REF = 'affiliateo_ref';
|
|
@@ -355,6 +356,12 @@ export function AffiliateoProvider({ appId, campaignId, apiUrl, allowedHostnames
|
|
|
355
356
|
gclid: params.get('gclid') || '',
|
|
356
357
|
screen_width: window.innerWidth,
|
|
357
358
|
};
|
|
359
|
+
// Attach the (single-use) Turnstile token if one is still unconsumed.
|
|
360
|
+
// Matters for consent flows where the host disabled auto-capture and
|
|
361
|
+
// this manual call is the session-opening event.
|
|
362
|
+
const tsToken = consumeTurnstileToken();
|
|
363
|
+
if (tsToken)
|
|
364
|
+
payload.cf_turnstile_token = tsToken;
|
|
358
365
|
queueRef.current?.enqueue(`${apiBase}/api/v1/web/event`, payload);
|
|
359
366
|
log('pageview (manual)', { path: pagePath });
|
|
360
367
|
}
|
|
@@ -674,6 +681,11 @@ export function AffiliateoProvider({ appId, campaignId, apiUrl, allowedHostnames
|
|
|
674
681
|
ssSet(SS_PV_THROTTLE, JSON.stringify({ url: href, time: now }));
|
|
675
682
|
lastSendTime = now;
|
|
676
683
|
ended = false;
|
|
684
|
+
// Attach the (single-use) Turnstile token to the session-opening
|
|
685
|
+
// pageview. consumeTurnstileToken() returns null after the first
|
|
686
|
+
// consume, so SPA navigations go out bare — the server trusts them
|
|
687
|
+
// because the visitor row was created behind the verified token.
|
|
688
|
+
const tsToken = consumeTurnstileToken();
|
|
677
689
|
queue.enqueue(endpoint, {
|
|
678
690
|
type: 'pageview',
|
|
679
691
|
campaign_id: appIdRef.current,
|
|
@@ -692,6 +704,7 @@ export function AffiliateoProvider({ appId, campaignId, apiUrl, allowedHostnames
|
|
|
692
704
|
ttclid: params.get('ttclid') || '',
|
|
693
705
|
gclid: params.get('gclid') || '',
|
|
694
706
|
screen_width: window.innerWidth,
|
|
707
|
+
...(tsToken ? { cf_turnstile_token: tsToken } : {}),
|
|
695
708
|
});
|
|
696
709
|
log('pageview', { path: window.location.pathname });
|
|
697
710
|
}
|
|
@@ -761,13 +774,30 @@ export function AffiliateoProvider({ appId, campaignId, apiUrl, allowedHostnames
|
|
|
761
774
|
}
|
|
762
775
|
}
|
|
763
776
|
}
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
777
|
+
// Kick off the Turnstile mint regardless of auto-capture config so a
|
|
778
|
+
// consent flow's later manual trackPageview() finds a token waiting.
|
|
779
|
+
// Never rejects; resolves with or without a token (see turnstile.ts).
|
|
780
|
+
let cancelled = false;
|
|
781
|
+
void mintTurnstileToken().then(() => {
|
|
782
|
+
// Send pageview on mount, deferred until the Turnstile mint settles so
|
|
783
|
+
// the session-opening event carries the token (SPA navigations reuse
|
|
784
|
+
// the resolved state and don't wait). captureInitialPageview === false
|
|
785
|
+
// skips this for cookie-consent flows where the host wants to defer
|
|
786
|
+
// until the user accepts (then call trackPageview() manually).
|
|
787
|
+
//
|
|
788
|
+
// sentRef is set HERE (not before the mint) so a StrictMode
|
|
789
|
+
// double-mount can't mark the pageview as sent and then tear down the
|
|
790
|
+
// queue before the deferred send ever ran — the surviving mount's
|
|
791
|
+
// callback sees sentRef still false and fires.
|
|
792
|
+
if (cancelled)
|
|
793
|
+
return;
|
|
794
|
+
if (sentRef.current)
|
|
795
|
+
return;
|
|
796
|
+
if (autoCfg.captureInitialPageview && autoCfg.enabled) {
|
|
797
|
+
sentRef.current = true;
|
|
798
|
+
sendPageview();
|
|
799
|
+
}
|
|
800
|
+
});
|
|
771
801
|
// Detect user leaving
|
|
772
802
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
773
803
|
window.addEventListener('pagehide', sendEnd);
|
|
@@ -899,6 +929,7 @@ export function AffiliateoProvider({ appId, campaignId, apiUrl, allowedHostnames
|
|
|
899
929
|
document.addEventListener('keydown', onKeydownForLinkDecoration, true);
|
|
900
930
|
}
|
|
901
931
|
return () => {
|
|
932
|
+
cancelled = true;
|
|
902
933
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
903
934
|
window.removeEventListener('pagehide', sendEnd);
|
|
904
935
|
window.removeEventListener('popstate', onUrlChange);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Turnstile token minting. Mirrors the hosted t.js tracker:
|
|
3
|
+
* mint ONE invisible token at SDK init and attach it to the first pageview.
|
|
4
|
+
* The server verifies the token before creating the visitor row (and before
|
|
5
|
+
* counting a pay-per-click web_traffic click — the billable path REQUIRES a
|
|
6
|
+
* genuine pass when TURNSTILE_SECRET is configured). Subsequent events from
|
|
7
|
+
* the same visitor_id are trusted because the row was created behind a
|
|
8
|
+
* verified token, so the cost is one mint + one verify per session.
|
|
9
|
+
*
|
|
10
|
+
* Tokens are single-use at Cloudflare's siteverify, so consumeToken()
|
|
11
|
+
* hands the token out exactly once — re-sending a used token would get the
|
|
12
|
+
* event rejected server-side, not warn-allowed.
|
|
13
|
+
*
|
|
14
|
+
* Failure modes: if the Turnstile script is blocked (ad blockers, strict
|
|
15
|
+
* CSP), errors, or times out, minting resolves with no token and events go
|
|
16
|
+
* out bare. The server's warn-allow mode (TURNSTILE_REQUIRE unset) lets
|
|
17
|
+
* those through; commission-on-sale (web_affiliate) tracking never depends
|
|
18
|
+
* on the token at all.
|
|
19
|
+
*/
|
|
20
|
+
interface TurnstileApi {
|
|
21
|
+
render: (el: HTMLElement, opts: Record<string, unknown>) => unknown;
|
|
22
|
+
}
|
|
23
|
+
declare global {
|
|
24
|
+
interface Window {
|
|
25
|
+
turnstile?: TurnstileApi;
|
|
26
|
+
__affiliateoTSLoaded?: () => void;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Load the Turnstile script (once) and run the invisible challenge. Resolves
|
|
31
|
+
* when a token is available OR the mint failed/timed out — never rejects, so
|
|
32
|
+
* callers can unconditionally `.then(sendPageview)`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mintTurnstileToken(): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Hand out the minted token exactly once (single-use at siteverify).
|
|
37
|
+
* Returns null when no token was minted or it was already consumed.
|
|
38
|
+
*/
|
|
39
|
+
export declare function consumeTurnstileToken(): string | null;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Turnstile token minting. Mirrors the hosted t.js tracker:
|
|
3
|
+
* mint ONE invisible token at SDK init and attach it to the first pageview.
|
|
4
|
+
* The server verifies the token before creating the visitor row (and before
|
|
5
|
+
* counting a pay-per-click web_traffic click — the billable path REQUIRES a
|
|
6
|
+
* genuine pass when TURNSTILE_SECRET is configured). Subsequent events from
|
|
7
|
+
* the same visitor_id are trusted because the row was created behind a
|
|
8
|
+
* verified token, so the cost is one mint + one verify per session.
|
|
9
|
+
*
|
|
10
|
+
* Tokens are single-use at Cloudflare's siteverify, so consumeToken()
|
|
11
|
+
* hands the token out exactly once — re-sending a used token would get the
|
|
12
|
+
* event rejected server-side, not warn-allowed.
|
|
13
|
+
*
|
|
14
|
+
* Failure modes: if the Turnstile script is blocked (ad blockers, strict
|
|
15
|
+
* CSP), errors, or times out, minting resolves with no token and events go
|
|
16
|
+
* out bare. The server's warn-allow mode (TURNSTILE_REQUIRE unset) lets
|
|
17
|
+
* those through; commission-on-sale (web_affiliate) tracking never depends
|
|
18
|
+
* on the token at all.
|
|
19
|
+
*/
|
|
20
|
+
// Public sitekey (safe to expose) — the same one public/t.js ships. The
|
|
21
|
+
// matching secret lives in TURNSTILE_SECRET on the server.
|
|
22
|
+
const TURNSTILE_SITEKEY = '0x4AAAAAADR5UW2x6axFd13p';
|
|
23
|
+
const TURNSTILE_SCRIPT_SRC = 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=__affiliateoTSLoaded&render=explicit';
|
|
24
|
+
// Hard deadline for the whole mint (script load + challenge). t.js relies on
|
|
25
|
+
// Turnstile's own timeout-callback, but a script tag that never loads never
|
|
26
|
+
// fires ANY callback — this timer makes sure the first pageview can't be
|
|
27
|
+
// held hostage by a blocked challenges.cloudflare.com.
|
|
28
|
+
const MINT_TIMEOUT_MS = 3000;
|
|
29
|
+
let token = null;
|
|
30
|
+
let minting = null;
|
|
31
|
+
/**
|
|
32
|
+
* Load the Turnstile script (once) and run the invisible challenge. Resolves
|
|
33
|
+
* when a token is available OR the mint failed/timed out — never rejects, so
|
|
34
|
+
* callers can unconditionally `.then(sendPageview)`.
|
|
35
|
+
*/
|
|
36
|
+
export function mintTurnstileToken() {
|
|
37
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
38
|
+
return Promise.resolve();
|
|
39
|
+
}
|
|
40
|
+
if (minting)
|
|
41
|
+
return minting;
|
|
42
|
+
minting = new Promise((resolve) => {
|
|
43
|
+
let settled = false;
|
|
44
|
+
const settle = () => {
|
|
45
|
+
if (settled)
|
|
46
|
+
return;
|
|
47
|
+
settled = true;
|
|
48
|
+
clearTimeout(deadline);
|
|
49
|
+
resolve();
|
|
50
|
+
};
|
|
51
|
+
const deadline = setTimeout(settle, MINT_TIMEOUT_MS);
|
|
52
|
+
const render = () => {
|
|
53
|
+
if (!window.turnstile) {
|
|
54
|
+
settle();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const holder = document.createElement('div');
|
|
59
|
+
holder.style.display = 'none';
|
|
60
|
+
document.body.appendChild(holder);
|
|
61
|
+
window.turnstile.render(holder, {
|
|
62
|
+
sitekey: TURNSTILE_SITEKEY,
|
|
63
|
+
// Invisible widget — never shows UI. Real browsers pass the
|
|
64
|
+
// challenge in 50-200ms with zero user interaction.
|
|
65
|
+
size: 'invisible',
|
|
66
|
+
callback: (tok) => {
|
|
67
|
+
token = tok;
|
|
68
|
+
settle();
|
|
69
|
+
},
|
|
70
|
+
'error-callback': settle,
|
|
71
|
+
'timeout-callback': settle,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
settle();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
if (window.turnstile) {
|
|
79
|
+
render();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const script = document.createElement('script');
|
|
84
|
+
script.src = TURNSTILE_SCRIPT_SRC;
|
|
85
|
+
script.async = true;
|
|
86
|
+
script.defer = true;
|
|
87
|
+
script.onerror = settle;
|
|
88
|
+
window.__affiliateoTSLoaded = render;
|
|
89
|
+
document.head.appendChild(script);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
settle();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
return minting;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Hand out the minted token exactly once (single-use at siteverify).
|
|
99
|
+
* Returns null when no token was minted or it was already consumed.
|
|
100
|
+
*/
|
|
101
|
+
export function consumeTurnstileToken() {
|
|
102
|
+
const t = token;
|
|
103
|
+
token = null;
|
|
104
|
+
return t;
|
|
105
|
+
}
|