@prosopo/procaptcha-frictionless 2.8.22 → 2.11.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/.turbo/turbo-build$colon$cjs.log +12 -14
- package/.turbo/turbo-build$colon$tsc.log +74 -0
- package/.turbo/turbo-build.log +17 -15
- package/CHANGELOG.md +791 -0
- package/dist/ProcaptchaFrictionless.d.ts +3 -0
- package/dist/ProcaptchaFrictionless.d.ts.map +1 -0
- package/dist/ProcaptchaFrictionless.js +78 -36
- package/dist/ProcaptchaFrictionless.js.map +1 -0
- package/dist/cjs/ProcaptchaFrictionless.cjs +97 -33
- package/dist/cjs/customDetectBot.cjs +49 -14
- package/dist/customDetectBot.d.ts +5 -0
- package/dist/customDetectBot.d.ts.map +1 -0
- package/dist/customDetectBot.js +50 -15
- package/dist/customDetectBot.js.map +1 -0
- package/dist/detectorLoader.d.ts +4 -0
- package/dist/detectorLoader.d.ts.map +1 -0
- package/dist/detectorLoader.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/tests/customDetectBot.test.d.ts +2 -0
- package/dist/tests/customDetectBot.test.d.ts.map +1 -0
- package/dist/tests/customDetectBot.test.js +45 -0
- package/dist/tests/customDetectBot.test.js.map +1 -0
- package/dist/tests/customDetectBotSimd.test.d.ts +2 -0
- package/dist/tests/customDetectBotSimd.test.d.ts.map +1 -0
- package/dist/tests/customDetectBotSimd.test.js +106 -0
- package/dist/tests/customDetectBotSimd.test.js.map +1 -0
- package/package.json +17 -14
- package/src/ProcaptchaFrictionless.tsx +286 -0
- package/src/customDetectBot.ts +160 -0
- package/src/detectorLoader.ts +18 -0
- package/src/index.ts +14 -0
- package/src/tests/customDetectBot.test.ts +77 -0
- package/src/tests/customDetectBotSimd.test.ts +159 -0
- package/tsconfig.cjs.json +48 -0
- package/tsconfig.json +55 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsconfig.types.json +9 -0
- package/vite.cjs.config.ts +1 -1
- package/vite.esm.config.ts +1 -1
- package/vite.test.config.ts +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
const mocks = vi.hoisted(() => ({
|
|
3
|
+
getFrictionlessCaptcha: vi.fn(),
|
|
4
|
+
getRandomActiveProvider: vi.fn(),
|
|
5
|
+
prefetchProviders: vi.fn(async () => undefined),
|
|
6
|
+
detect: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("@prosopo/api", () => ({
|
|
9
|
+
ProviderApi: vi.fn(() => ({
|
|
10
|
+
getFrictionlessCaptcha: mocks.getFrictionlessCaptcha,
|
|
11
|
+
})),
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("@prosopo/load-balancer", () => ({
|
|
14
|
+
getRandomActiveProvider: mocks.getRandomActiveProvider,
|
|
15
|
+
prefetchProviders: mocks.prefetchProviders,
|
|
16
|
+
}));
|
|
17
|
+
vi.mock("@prosopo/procaptcha-common", () => ({
|
|
18
|
+
ExtensionLoader: vi.fn(async () => {
|
|
19
|
+
return class FakeExtension {
|
|
20
|
+
getAccount() {
|
|
21
|
+
return Promise.resolve({
|
|
22
|
+
account: { address: "5FakeUserAccountAddress" },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
vi.mock("../detectorLoader.js", () => ({
|
|
29
|
+
DetectorLoader: vi.fn(async () => mocks.detect),
|
|
30
|
+
}));
|
|
31
|
+
import customDetectBot from "../customDetectBot.js";
|
|
32
|
+
const baseConfig = {
|
|
33
|
+
account: { address: "5FakeSiteKey" },
|
|
34
|
+
defaultEnvironment: "production",
|
|
35
|
+
web2: true,
|
|
36
|
+
mode: "frictionless",
|
|
37
|
+
};
|
|
38
|
+
const makeDetectionResult = (getSimdReadings) => ({
|
|
39
|
+
token: "TOKEN",
|
|
40
|
+
encryptHeadHash: "HASH",
|
|
41
|
+
userAccount: { account: { address: "5FakeUserAccountAddress" } },
|
|
42
|
+
provider: {
|
|
43
|
+
provider: { url: "https://provider.test" },
|
|
44
|
+
providerAccount: "5Provider",
|
|
45
|
+
},
|
|
46
|
+
getSimdReadings,
|
|
47
|
+
mouseTracker: undefined,
|
|
48
|
+
touchTracker: undefined,
|
|
49
|
+
clickTracker: undefined,
|
|
50
|
+
});
|
|
51
|
+
const captchaResponse = {
|
|
52
|
+
captchaType: "pow",
|
|
53
|
+
sessionId: "SID",
|
|
54
|
+
status: "ok",
|
|
55
|
+
};
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
mocks.getFrictionlessCaptcha.mockReset();
|
|
58
|
+
mocks.getRandomActiveProvider.mockReset();
|
|
59
|
+
mocks.prefetchProviders.mockReset();
|
|
60
|
+
mocks.detect.mockReset();
|
|
61
|
+
mocks.prefetchProviders.mockResolvedValue(undefined);
|
|
62
|
+
mocks.getFrictionlessCaptcha.mockResolvedValue(captchaResponse);
|
|
63
|
+
});
|
|
64
|
+
describe("customDetectBot SIMD deferral", () => {
|
|
65
|
+
it("passes simdReadings as undefined to the frictionless POST even when getSimdReadings would return a value", async () => {
|
|
66
|
+
const getSimdReadings = vi.fn().mockResolvedValue("ENCODED_SIMD");
|
|
67
|
+
mocks.detect.mockResolvedValue(makeDetectionResult(getSimdReadings));
|
|
68
|
+
await customDetectBot(baseConfig, undefined, () => undefined);
|
|
69
|
+
expect(mocks.getFrictionlessCaptcha).toHaveBeenCalledTimes(1);
|
|
70
|
+
const args = mocks.getFrictionlessCaptcha.mock.calls[0];
|
|
71
|
+
expect(args?.[5]).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
it("fires getSimdReadings after the frictionless POST is initiated (fire-and-forget)", async () => {
|
|
74
|
+
const callOrder = [];
|
|
75
|
+
mocks.getFrictionlessCaptcha.mockImplementation(() => {
|
|
76
|
+
callOrder.push("captcha-call");
|
|
77
|
+
return Promise.resolve(captchaResponse);
|
|
78
|
+
});
|
|
79
|
+
const getSimdReadings = vi.fn().mockImplementation(() => {
|
|
80
|
+
callOrder.push("simd-call");
|
|
81
|
+
return Promise.resolve("ENCODED_SIMD");
|
|
82
|
+
});
|
|
83
|
+
mocks.detect.mockResolvedValue(makeDetectionResult(getSimdReadings));
|
|
84
|
+
await customDetectBot(baseConfig, undefined, () => undefined);
|
|
85
|
+
expect(getSimdReadings).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(callOrder.indexOf("captcha-call")).toBeLessThan(callOrder.indexOf("simd-call"));
|
|
87
|
+
});
|
|
88
|
+
it("does not block the frictionless POST on a slow getSimdReadings", async () => {
|
|
89
|
+
let simdResolve;
|
|
90
|
+
const simdPromise = new Promise((resolve) => {
|
|
91
|
+
simdResolve = resolve;
|
|
92
|
+
});
|
|
93
|
+
const getSimdReadings = vi.fn().mockReturnValue(simdPromise);
|
|
94
|
+
mocks.detect.mockResolvedValue(makeDetectionResult(getSimdReadings));
|
|
95
|
+
const result = await customDetectBot(baseConfig, undefined, () => undefined);
|
|
96
|
+
expect(result.sessionId).toBe("SID");
|
|
97
|
+
simdResolve("ENCODED_SIMD");
|
|
98
|
+
});
|
|
99
|
+
it("skips the SIMD trigger when the detector doesn't expose getSimdReadings", async () => {
|
|
100
|
+
mocks.detect.mockResolvedValue(makeDetectionResult(undefined));
|
|
101
|
+
const result = await customDetectBot(baseConfig, undefined, () => undefined);
|
|
102
|
+
expect(result.sessionId).toBe("SID");
|
|
103
|
+
expect(mocks.getFrictionlessCaptcha).toHaveBeenCalledTimes(1);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
//# sourceMappingURL=customDetectBotSimd.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customDetectBotSimd.test.js","sourceRoot":"","sources":["../../src/tests/customDetectBotSimd.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prosopo/procaptcha-frictionless",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"author": "PROSOPO LIMITED <info@prosopo.io>",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,28 +22,30 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"clean": "del-cli --verbose dist tsconfig.tsbuildinfo",
|
|
24
24
|
"test": "NODE_ENV=${NODE_ENV:-test}; npx vitest run --config ./vite.test.config.ts",
|
|
25
|
-
"build": "
|
|
25
|
+
"build": "npm run build:cross-env -- --mode ${NODE_ENV:-development}",
|
|
26
|
+
"build:cross-env": "vite build --config vite.esm.config.ts",
|
|
26
27
|
"build:tsc": "tsc --build --verbose",
|
|
27
28
|
"build:cjs": "NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV",
|
|
28
29
|
"typecheck": "tsc --project tsconfig.types.json"
|
|
29
30
|
},
|
|
30
31
|
"browserslist": ["> 0.5%, last 2 versions, not dead"],
|
|
31
32
|
"dependencies": {
|
|
32
|
-
"@prosopo/api": "3.
|
|
33
|
-
"@prosopo/common": "3.1.
|
|
34
|
-
"@prosopo/detector": "3.
|
|
35
|
-
"@prosopo/load-balancer": "2.
|
|
36
|
-
"@prosopo/locale": "3.
|
|
37
|
-
"@prosopo/procaptcha-common": "2.
|
|
38
|
-
"@prosopo/procaptcha-pow": "2.
|
|
39
|
-
"@prosopo/procaptcha-
|
|
40
|
-
"@prosopo/
|
|
41
|
-
"@prosopo/
|
|
33
|
+
"@prosopo/api": "3.4.8",
|
|
34
|
+
"@prosopo/common": "3.1.38",
|
|
35
|
+
"@prosopo/detector": "3.4.36",
|
|
36
|
+
"@prosopo/load-balancer": "2.9.10",
|
|
37
|
+
"@prosopo/locale": "3.2.4",
|
|
38
|
+
"@prosopo/procaptcha-common": "2.10.17",
|
|
39
|
+
"@prosopo/procaptcha-pow": "2.9.4",
|
|
40
|
+
"@prosopo/procaptcha-puzzle": "2.10.8",
|
|
41
|
+
"@prosopo/procaptcha-react": "2.9.66",
|
|
42
|
+
"@prosopo/types": "4.3.0",
|
|
43
|
+
"@prosopo/widget-skeleton": "2.8.3",
|
|
42
44
|
"dotenv": "16.4.5",
|
|
43
45
|
"react": "18.3.1"
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
46
|
-
"@prosopo/config": "3.1
|
|
48
|
+
"@prosopo/config": "3.3.1",
|
|
47
49
|
"@types/node": "22.10.2",
|
|
48
50
|
"@vitest/coverage-v8": "3.2.4",
|
|
49
51
|
"concurrently": "9.0.1",
|
|
@@ -57,7 +59,8 @@
|
|
|
57
59
|
},
|
|
58
60
|
"repository": {
|
|
59
61
|
"type": "git",
|
|
60
|
-
"url": "git+https://github.com/prosopo/captcha.git"
|
|
62
|
+
"url": "git+https://github.com/prosopo/captcha.git",
|
|
63
|
+
"directory": "packages/procaptcha-frictionless"
|
|
61
64
|
},
|
|
62
65
|
"bugs": {
|
|
63
66
|
"url": "https://github.com/prosopo/captcha/issues"
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import { loadI18next } from "@prosopo/locale";
|
|
16
|
+
import {
|
|
17
|
+
Checkbox,
|
|
18
|
+
TestModeBanner,
|
|
19
|
+
getDefaultEvents,
|
|
20
|
+
isSecureBrowserContext,
|
|
21
|
+
providerRetry,
|
|
22
|
+
} from "@prosopo/procaptcha-common";
|
|
23
|
+
import {
|
|
24
|
+
CaptchaType,
|
|
25
|
+
type FrictionlessState,
|
|
26
|
+
type ModeType,
|
|
27
|
+
ProcaptchaConfigSchema,
|
|
28
|
+
type ProcaptchaFrictionlessProps,
|
|
29
|
+
} from "@prosopo/types";
|
|
30
|
+
import { darkTheme, lightTheme } from "@prosopo/widget-skeleton";
|
|
31
|
+
import { useEffect, useRef, useState } from "react";
|
|
32
|
+
import customDetectBot from "./customDetectBot.js";
|
|
33
|
+
|
|
34
|
+
// Each session uses exactly one solver — chosen by the /frictionless response.
|
|
35
|
+
const ProcaptchaLoader = async () =>
|
|
36
|
+
(await import("@prosopo/procaptcha-react")).Procaptcha;
|
|
37
|
+
const ProcaptchaPuzzleLoader = async () =>
|
|
38
|
+
(await import("@prosopo/procaptcha-puzzle")).ProcaptchaPuzzle;
|
|
39
|
+
const ProcaptchaPowLoader = async () =>
|
|
40
|
+
(await import("@prosopo/procaptcha-pow")).ProcaptchaPow;
|
|
41
|
+
|
|
42
|
+
const renderPlaceholder = (
|
|
43
|
+
theme: string | undefined,
|
|
44
|
+
mode: ModeType,
|
|
45
|
+
errorMessage: string | undefined,
|
|
46
|
+
isTranslationLoaded: boolean,
|
|
47
|
+
translationFn: (key: string) => string,
|
|
48
|
+
loading: boolean,
|
|
49
|
+
) => {
|
|
50
|
+
const checkboxTheme = "light" === theme ? lightTheme : darkTheme;
|
|
51
|
+
|
|
52
|
+
if (mode === "invisible") {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Checkbox
|
|
58
|
+
theme={checkboxTheme}
|
|
59
|
+
onChange={async () => {}}
|
|
60
|
+
checked={false}
|
|
61
|
+
labelText={isTranslationLoaded ? translationFn("WIDGET.I_AM_HUMAN") : ""}
|
|
62
|
+
error={errorMessage}
|
|
63
|
+
aria-label="human checkbox"
|
|
64
|
+
loading={loading}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type FrictionlessLoadingState = {
|
|
70
|
+
loading: boolean;
|
|
71
|
+
attemptCount: number;
|
|
72
|
+
errorMessage?: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const defaultLoadingState = (
|
|
76
|
+
attemptCount: number,
|
|
77
|
+
): FrictionlessLoadingState => ({
|
|
78
|
+
loading: false,
|
|
79
|
+
attemptCount: attemptCount || 0,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const ProcaptchaFrictionless = ({
|
|
83
|
+
config,
|
|
84
|
+
callbacks,
|
|
85
|
+
restart,
|
|
86
|
+
i18n,
|
|
87
|
+
detectBot = customDetectBot,
|
|
88
|
+
container,
|
|
89
|
+
}: ProcaptchaFrictionlessProps) => {
|
|
90
|
+
const stateRef = useRef(defaultLoadingState(0));
|
|
91
|
+
const events = getDefaultEvents(callbacks);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (config.language) {
|
|
95
|
+
if (i18n) {
|
|
96
|
+
if (i18n.language !== config.language) {
|
|
97
|
+
i18n.changeLanguage(config.language).then((r) => r);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
loadI18next(false).then((i18n) => {
|
|
101
|
+
if (i18n.language !== config.language)
|
|
102
|
+
i18n.changeLanguage(config.language).then((r) => r);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}, [i18n, config.language]);
|
|
107
|
+
|
|
108
|
+
const [componentToRender, setComponentToRender] = useState(
|
|
109
|
+
renderPlaceholder(
|
|
110
|
+
config.theme,
|
|
111
|
+
config.mode,
|
|
112
|
+
stateRef.current.errorMessage,
|
|
113
|
+
i18n.isInitialized,
|
|
114
|
+
i18n.t,
|
|
115
|
+
true,
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const resetState = (attemptCount?: number) => {
|
|
120
|
+
stateRef.current = defaultLoadingState(
|
|
121
|
+
attemptCount || stateRef.current.attemptCount,
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const fallOverWithStyle = (errorMessage?: string, errorKey?: string) => {
|
|
126
|
+
// We could always re-render here after a period but this will result in never-ending requests to Providers when
|
|
127
|
+
// settings are incorrect, or the user is not human. We need to selectively re-render for events like
|
|
128
|
+
// `no session found` but not for other errors.
|
|
129
|
+
if (errorKey === "CAPTCHA.NO_SESSION_FOUND") {
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
restartComponentTimeout();
|
|
132
|
+
}, 0);
|
|
133
|
+
}
|
|
134
|
+
setComponentToRender(
|
|
135
|
+
renderPlaceholder(
|
|
136
|
+
config.theme,
|
|
137
|
+
config.mode,
|
|
138
|
+
errorMessage || "Cannot load CAPTCHA",
|
|
139
|
+
i18n.isInitialized,
|
|
140
|
+
i18n.t,
|
|
141
|
+
false,
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const restartComponentTimeout = () => {
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
resetState(0);
|
|
149
|
+
events.onReset();
|
|
150
|
+
// `restart` frictionless widget after 10 seconds
|
|
151
|
+
restart();
|
|
152
|
+
}, 10000);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Mount the captcha widget that matches the chosen type. Used both for the
|
|
156
|
+
// initial frictionless decision and for the post-pow escalation handoff —
|
|
157
|
+
// in the latter case the FrictionlessState carries the new sessionId minted
|
|
158
|
+
// by the provider when it decided PoW alone wasn't enough.
|
|
159
|
+
const renderForCaptchaType = async (
|
|
160
|
+
captchaType: string,
|
|
161
|
+
frictionlessState: FrictionlessState,
|
|
162
|
+
) => {
|
|
163
|
+
const onEscalate = (
|
|
164
|
+
next: CaptchaType.image | CaptchaType.puzzle,
|
|
165
|
+
newSessionId: string,
|
|
166
|
+
) => {
|
|
167
|
+
void renderForCaptchaType(next, {
|
|
168
|
+
...frictionlessState,
|
|
169
|
+
sessionId: newSessionId,
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (captchaType === CaptchaType.image) {
|
|
174
|
+
const Procaptcha = await ProcaptchaLoader();
|
|
175
|
+
setComponentToRender(
|
|
176
|
+
<Procaptcha
|
|
177
|
+
config={config}
|
|
178
|
+
callbacks={callbacks}
|
|
179
|
+
frictionlessState={frictionlessState}
|
|
180
|
+
i18n={i18n}
|
|
181
|
+
/>,
|
|
182
|
+
);
|
|
183
|
+
} else if (captchaType === CaptchaType.puzzle) {
|
|
184
|
+
const ProcaptchaPuzzle = await ProcaptchaPuzzleLoader();
|
|
185
|
+
setComponentToRender(
|
|
186
|
+
<ProcaptchaPuzzle
|
|
187
|
+
config={config}
|
|
188
|
+
callbacks={callbacks}
|
|
189
|
+
frictionlessState={frictionlessState}
|
|
190
|
+
i18n={i18n}
|
|
191
|
+
/>,
|
|
192
|
+
);
|
|
193
|
+
} else {
|
|
194
|
+
const ProcaptchaPow = await ProcaptchaPowLoader();
|
|
195
|
+
setComponentToRender(
|
|
196
|
+
<ProcaptchaPow
|
|
197
|
+
config={config}
|
|
198
|
+
callbacks={callbacks}
|
|
199
|
+
frictionlessState={frictionlessState}
|
|
200
|
+
i18n={i18n}
|
|
201
|
+
onEscalate={onEscalate}
|
|
202
|
+
/>,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const start = async () => {
|
|
208
|
+
// Procaptcha cannot run over plain HTTP (no SubtleCrypto etc.), which
|
|
209
|
+
// would otherwise fail later with a cryptic provider-selection error.
|
|
210
|
+
// Surface a clear, non-retrying message instead.
|
|
211
|
+
if (!isSecureBrowserContext()) {
|
|
212
|
+
const errorMessage = i18n.isInitialized
|
|
213
|
+
? i18n.t("WIDGET.INSECURE_CONTEXT")
|
|
214
|
+
: "Procaptcha requires a secure (HTTPS) connection";
|
|
215
|
+
events.onError(new Error(errorMessage));
|
|
216
|
+
fallOverWithStyle(errorMessage, "WIDGET.INSECURE_CONTEXT");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
await providerRetry(
|
|
221
|
+
async () => {
|
|
222
|
+
stateRef.current.attemptCount += 1;
|
|
223
|
+
|
|
224
|
+
const configOutput = ProcaptchaConfigSchema.parse(config);
|
|
225
|
+
const result = await detectBot(configOutput, container, restart);
|
|
226
|
+
|
|
227
|
+
if (result.error?.message) {
|
|
228
|
+
stateRef.current = {
|
|
229
|
+
...stateRef.current,
|
|
230
|
+
loading: false,
|
|
231
|
+
errorMessage: result.error?.message,
|
|
232
|
+
};
|
|
233
|
+
events.onError(new Error(result.error?.message));
|
|
234
|
+
fallOverWithStyle(result.error?.message, result.error?.key);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const frictionlessState: FrictionlessState = {
|
|
239
|
+
provider: result.provider,
|
|
240
|
+
sessionId: result.sessionId,
|
|
241
|
+
userAccount: result.userAccount,
|
|
242
|
+
restart, // Pass restart function
|
|
243
|
+
behaviorCollector1: result.behaviorCollector1,
|
|
244
|
+
behaviorCollector2: result.behaviorCollector2,
|
|
245
|
+
behaviorCollector3: result.behaviorCollector3,
|
|
246
|
+
deviceCapability: result.deviceCapability,
|
|
247
|
+
encryptBehavioralData: result.encryptBehavioralData,
|
|
248
|
+
getSimdReadings: result.getSimdReadings,
|
|
249
|
+
hp: result.hp,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
await renderForCaptchaType(result.captchaType, frictionlessState);
|
|
253
|
+
|
|
254
|
+
stateRef.current = {
|
|
255
|
+
...stateRef.current,
|
|
256
|
+
loading: false,
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
start,
|
|
260
|
+
resetState,
|
|
261
|
+
stateRef.current.attemptCount,
|
|
262
|
+
5,
|
|
263
|
+
).finally(() => {
|
|
264
|
+
if (stateRef.current.attemptCount >= 5) {
|
|
265
|
+
fallOverWithStyle();
|
|
266
|
+
restartComponentTimeout();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
const detectAndSetComponent = async () => {
|
|
274
|
+
await start();
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
detectAndSetComponent();
|
|
278
|
+
}, [config, callbacks, detectBot, config.language]);
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<>
|
|
282
|
+
<TestModeBanner siteKey={config.account?.address ?? ""} />
|
|
283
|
+
{componentToRender}
|
|
284
|
+
</>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import { ProviderApi } from "@prosopo/api";
|
|
16
|
+
import { ProsopoEnvError } from "@prosopo/common";
|
|
17
|
+
import {
|
|
18
|
+
getRandomActiveProvider,
|
|
19
|
+
prefetchProviders,
|
|
20
|
+
} from "@prosopo/load-balancer";
|
|
21
|
+
import { ExtensionLoader } from "@prosopo/procaptcha-common";
|
|
22
|
+
import { EnvironmentTypesSchema } from "@prosopo/types";
|
|
23
|
+
import type {
|
|
24
|
+
BotDetectionFunction,
|
|
25
|
+
ProcaptchaClientConfigOutput,
|
|
26
|
+
} from "@prosopo/types";
|
|
27
|
+
import type { BotDetectionFunctionResult } from "@prosopo/types";
|
|
28
|
+
import { DetectorLoader } from "./detectorLoader.js";
|
|
29
|
+
|
|
30
|
+
if (typeof window !== "undefined") {
|
|
31
|
+
const envHint =
|
|
32
|
+
typeof process !== "undefined"
|
|
33
|
+
? process.env?.PROSOPO_DEFAULT_ENVIRONMENT
|
|
34
|
+
: undefined;
|
|
35
|
+
const parsedEnv = EnvironmentTypesSchema.safeParse(envHint);
|
|
36
|
+
if (parsedEnv.success) {
|
|
37
|
+
prefetchProviders(parsedEnv.data).catch(() => undefined);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const withTimeout = async <T>(
|
|
42
|
+
promise: Promise<T>,
|
|
43
|
+
ms: number,
|
|
44
|
+
): Promise<T> => {
|
|
45
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
46
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
47
|
+
timeoutId = setTimeout(() => {
|
|
48
|
+
reject(new ProsopoEnvError("API.UNKNOWN"));
|
|
49
|
+
}, ms);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const result = await Promise.race([promise, timeoutPromise]);
|
|
54
|
+
if (timeoutId) {
|
|
55
|
+
clearTimeout(timeoutId);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (timeoutId) {
|
|
60
|
+
clearTimeout(timeoutId);
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const customDetectBot: BotDetectionFunction = async (
|
|
67
|
+
config: ProcaptchaClientConfigOutput,
|
|
68
|
+
container: HTMLElement | undefined,
|
|
69
|
+
restartFn: () => void,
|
|
70
|
+
): Promise<BotDetectionFunctionResult> => {
|
|
71
|
+
const [ExtClass, detect] = await Promise.all([
|
|
72
|
+
ExtensionLoader(config.web2),
|
|
73
|
+
DetectorLoader(),
|
|
74
|
+
prefetchProviders(config.defaultEnvironment),
|
|
75
|
+
]);
|
|
76
|
+
const ext = new ExtClass();
|
|
77
|
+
|
|
78
|
+
const detectionResult = await detect(
|
|
79
|
+
config.defaultEnvironment,
|
|
80
|
+
getRandomActiveProvider,
|
|
81
|
+
container,
|
|
82
|
+
restartFn,
|
|
83
|
+
() => ext.getAccount(config),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const userAccount = detectionResult.userAccount;
|
|
87
|
+
|
|
88
|
+
if (!config.account.address) {
|
|
89
|
+
throw new ProsopoEnvError("GENERAL.SITE_KEY_MISSING");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Get random active provider with timeout
|
|
93
|
+
const provider = detectionResult.provider;
|
|
94
|
+
|
|
95
|
+
if (!provider) {
|
|
96
|
+
throw new Error("Provider Selection Failed");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const providerApi = new ProviderApi(
|
|
100
|
+
provider.provider.url,
|
|
101
|
+
config.account.address,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// SIMD readings deliberately omitted from the frictionless hop. The WASM
|
|
105
|
+
// benchmark is a CPU-bound loop that contends with BotScoreWorker if it
|
|
106
|
+
// runs during detection; deferring it until after the POST is in flight
|
|
107
|
+
// lets it complete in the worker thread while the network round-trip
|
|
108
|
+
// burns. Readings still attach on the challenge GET and on solution
|
|
109
|
+
// submit (first-hop-wins server-side).
|
|
110
|
+
const captchaPromise = providerApi.getFrictionlessCaptcha(
|
|
111
|
+
detectionResult.token,
|
|
112
|
+
detectionResult.encryptHeadHash,
|
|
113
|
+
config.account.address,
|
|
114
|
+
userAccount.account.address,
|
|
115
|
+
config.mode,
|
|
116
|
+
undefined,
|
|
117
|
+
);
|
|
118
|
+
if (detectionResult.getSimdReadings) {
|
|
119
|
+
// Fire-and-forget: triggers the memoised prefetch inside the catcher
|
|
120
|
+
// so the next hop sees a hot benchmark. We never await the result here.
|
|
121
|
+
void detectionResult.getSimdReadings(60_000).catch(() => undefined);
|
|
122
|
+
}
|
|
123
|
+
const captcha = await withTimeout(captchaPromise, 10000);
|
|
124
|
+
|
|
125
|
+
// Fire-and-forget DNS observation beacon. Failures swallowed —
|
|
126
|
+
// observation must never break the captcha flow.
|
|
127
|
+
if (captcha.dns_url) {
|
|
128
|
+
try {
|
|
129
|
+
void fetch(captcha.dns_url, {
|
|
130
|
+
method: "GET",
|
|
131
|
+
mode: "no-cors",
|
|
132
|
+
credentials: "omit",
|
|
133
|
+
keepalive: true,
|
|
134
|
+
cache: "no-store",
|
|
135
|
+
}).catch(() => undefined);
|
|
136
|
+
} catch {
|
|
137
|
+
/* swallow */
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
captchaType: captcha.captchaType,
|
|
143
|
+
sessionId: captcha.sessionId,
|
|
144
|
+
provider: provider,
|
|
145
|
+
status: captcha.status,
|
|
146
|
+
userAccount: userAccount,
|
|
147
|
+
error: captcha.error,
|
|
148
|
+
hp: captcha.hp,
|
|
149
|
+
// Map specific trackers to generic behavioral collectors
|
|
150
|
+
behaviorCollector1: detectionResult.mouseTracker,
|
|
151
|
+
behaviorCollector2: detectionResult.touchTracker,
|
|
152
|
+
behaviorCollector3: detectionResult.clickTracker,
|
|
153
|
+
deviceCapability: detectionResult.hasTouchSupport,
|
|
154
|
+
encryptBehavioralData: detectionResult.encryptBehavioralData,
|
|
155
|
+
packBehavioralData: detectionResult.packBehavioralData,
|
|
156
|
+
getSimdReadings: detectionResult.getSimdReadings,
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export default customDetectBot;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
type DetectorType = typeof import("@prosopo/detector").default;
|
|
16
|
+
|
|
17
|
+
export const DetectorLoader = async (): Promise<DetectorType> =>
|
|
18
|
+
(await import("@prosopo/detector")).default;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
export * from "./ProcaptchaFrictionless.js";
|