@prosopo/procaptcha-common 2.9.19 → 2.10.17
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 +22 -15
- package/.turbo/turbo-build$colon$tsc.log +52 -0
- package/.turbo/turbo-build.log +26 -16
- package/CHANGELOG.md +392 -0
- package/dist/callbacks/defaultCallbacks.d.ts +4 -0
- package/dist/callbacks/defaultCallbacks.d.ts.map +1 -0
- package/dist/callbacks/defaultCallbacks.js.map +1 -0
- package/dist/callbacks/defaultEvents.d.ts +14 -0
- package/dist/callbacks/defaultEvents.d.ts.map +1 -0
- package/dist/callbacks/defaultEvents.js.map +1 -0
- package/dist/cjs/elements/form.cjs +8 -2
- package/dist/cjs/elements/window.cjs +7 -0
- package/dist/cjs/index.cjs +5 -0
- package/dist/cjs/reactComponents/Checkbox.cjs +11 -7
- package/dist/cjs/reactComponents/Honeypot.cjs +67 -0
- package/dist/cjs/reactComponents/TestModeBanner.cjs +47 -0
- package/dist/elements/form.d.ts +5 -0
- package/dist/elements/form.d.ts.map +1 -0
- package/dist/elements/form.js +8 -2
- package/dist/elements/form.js.map +1 -0
- package/dist/elements/window.d.ts +3 -0
- package/dist/elements/window.d.ts.map +1 -0
- package/dist/elements/window.js +8 -1
- package/dist/elements/window.js.map +1 -0
- package/dist/extensionLoader.d.ts +2 -0
- package/dist/extensionLoader.d.ts.map +1 -0
- package/dist/extensionLoader.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -0
- package/dist/providers.d.ts +4 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js.map +1 -0
- package/dist/reactComponents/Checkbox.d.ts +13 -0
- package/dist/reactComponents/Checkbox.d.ts.map +1 -0
- package/dist/reactComponents/Checkbox.js +11 -7
- package/dist/reactComponents/Checkbox.js.map +1 -0
- package/dist/reactComponents/Honeypot.d.ts +6 -0
- package/dist/reactComponents/Honeypot.d.ts.map +1 -0
- package/dist/reactComponents/Honeypot.js +67 -0
- package/dist/reactComponents/Honeypot.js.map +1 -0
- package/dist/reactComponents/Reload.d.ts +8 -0
- package/dist/reactComponents/Reload.d.ts.map +1 -0
- package/dist/reactComponents/Reload.js.map +1 -0
- package/dist/reactComponents/TestModeBanner.d.ts +7 -0
- package/dist/reactComponents/TestModeBanner.d.ts.map +1 -0
- package/dist/reactComponents/TestModeBanner.js +47 -0
- package/dist/reactComponents/TestModeBanner.js.map +1 -0
- package/dist/state/builder.d.ts +9 -0
- package/dist/state/builder.d.ts.map +1 -0
- package/dist/state/builder.js.map +1 -0
- package/dist/tests/defaultCallbacks.test.d.ts +2 -0
- package/dist/tests/defaultCallbacks.test.d.ts.map +1 -0
- package/dist/tests/defaultCallbacks.test.js +219 -0
- package/dist/tests/defaultCallbacks.test.js.map +1 -0
- package/dist/tests/defaultEvents.test.d.ts +2 -0
- package/dist/tests/defaultEvents.test.d.ts.map +1 -0
- package/dist/tests/defaultEvents.test.js +54 -0
- package/dist/tests/defaultEvents.test.js.map +1 -0
- package/dist/tests/extensionLoader.test.d.ts +2 -0
- package/dist/tests/extensionLoader.test.d.ts.map +1 -0
- package/dist/tests/extensionLoader.test.js +21 -0
- package/dist/tests/extensionLoader.test.js.map +1 -0
- package/dist/tests/form.test.d.ts +2 -0
- package/dist/tests/form.test.d.ts.map +1 -0
- package/dist/tests/form.test.js +98 -0
- package/dist/tests/form.test.js.map +1 -0
- package/dist/tests/providers.test.d.ts +2 -0
- package/dist/tests/providers.test.d.ts.map +1 -0
- package/dist/tests/providers.test.js +111 -0
- package/dist/tests/providers.test.js.map +1 -0
- package/dist/tests/state-builder.test.d.ts +2 -0
- package/dist/tests/state-builder.test.d.ts.map +1 -0
- package/dist/tests/state-builder.test.js +193 -0
- package/dist/tests/state-builder.test.js.map +1 -0
- package/dist/tests/window.test.d.ts +2 -0
- package/dist/tests/window.test.d.ts.map +1 -0
- package/dist/tests/window.test.js +80 -0
- package/dist/tests/window.test.js.map +1 -0
- package/package.json +13 -9
- package/src/callbacks/defaultCallbacks.ts +197 -0
- package/src/callbacks/defaultEvents.ts +21 -0
- package/src/elements/form.ts +41 -0
- package/src/elements/window.ts +39 -0
- package/src/extensionLoader.ts +17 -0
- package/src/index.ts +24 -0
- package/src/providers.ts +49 -0
- package/src/reactComponents/Checkbox.tsx +203 -0
- package/src/reactComponents/Honeypot.tsx +133 -0
- package/src/reactComponents/Reload.tsx +99 -0
- package/src/reactComponents/TestModeBanner.tsx +75 -0
- package/src/state/builder.ts +137 -0
- package/src/tests/defaultCallbacks.test.ts +372 -0
- package/src/tests/defaultEvents.test.ts +80 -0
- package/src/tests/extensionLoader.test.ts +41 -0
- package/src/tests/form.test.ts +154 -0
- package/src/tests/providers.test.ts +175 -0
- package/src/tests/state-builder.test.ts +264 -0
- package/src/tests/window.test.ts +137 -0
- package/tsconfig.cjs.json +32 -0
- package/tsconfig.json +33 -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,17 @@
|
|
|
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 const ExtensionLoader = async (web2: boolean) =>
|
|
15
|
+
web2
|
|
16
|
+
? (await import("@prosopo/account/extension/ExtensionWeb2")).default
|
|
17
|
+
: (await import("@prosopo/account/extension/ExtensionWeb3")).default;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
export * from "./providers.js";
|
|
16
|
+
export * from "./state/builder.js";
|
|
17
|
+
export * from "./callbacks/defaultCallbacks.js";
|
|
18
|
+
export * from "./callbacks/defaultEvents.js";
|
|
19
|
+
export * from "./extensionLoader.js";
|
|
20
|
+
export * from "./elements/window.js";
|
|
21
|
+
export * from "./reactComponents/Reload.js";
|
|
22
|
+
export * from "./reactComponents/Checkbox.js";
|
|
23
|
+
export * from "./reactComponents/Honeypot.js";
|
|
24
|
+
export * from "./reactComponents/TestModeBanner.js";
|
package/src/providers.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
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 { getRandomActiveProvider } from "@prosopo/load-balancer";
|
|
16
|
+
import type { EnvironmentTypes } from "@prosopo/types";
|
|
17
|
+
|
|
18
|
+
export const getProcaptchaRandomActiveProvider = async (
|
|
19
|
+
defaultEnvironment: EnvironmentTypes,
|
|
20
|
+
) => {
|
|
21
|
+
const randomNumberU8a = window.crypto.getRandomValues(new Uint8Array(10));
|
|
22
|
+
const randomNumber = randomNumberU8a.reduce((a, b) => a + b, 0);
|
|
23
|
+
return await getRandomActiveProvider(defaultEnvironment, randomNumber);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const providerRetry = async (
|
|
27
|
+
currentFn: () => Promise<void>,
|
|
28
|
+
retryFn: () => Promise<void>,
|
|
29
|
+
stateReset: () => void,
|
|
30
|
+
attemptCount: number,
|
|
31
|
+
retryMax: number,
|
|
32
|
+
) => {
|
|
33
|
+
try {
|
|
34
|
+
await currentFn();
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (attemptCount >= retryMax) {
|
|
37
|
+
console.error(err);
|
|
38
|
+
console.error(
|
|
39
|
+
`Max retries (${attemptCount} of ${retryMax}) reached, aborting`,
|
|
40
|
+
);
|
|
41
|
+
return stateReset();
|
|
42
|
+
}
|
|
43
|
+
console.error(err);
|
|
44
|
+
// hit an error, disallow user's claim to be human
|
|
45
|
+
stateReset();
|
|
46
|
+
// trigger a retry to attempt a new provider until it passes
|
|
47
|
+
await retryFn();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
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 { css } from "@emotion/react";
|
|
16
|
+
import styled from "@emotion/styled";
|
|
17
|
+
import {
|
|
18
|
+
type Theme,
|
|
19
|
+
WIDGET_CHECKBOX_SPINNER_CSS_CLASS,
|
|
20
|
+
} from "@prosopo/widget-skeleton";
|
|
21
|
+
import {
|
|
22
|
+
type ButtonHTMLAttributes,
|
|
23
|
+
type CSSProperties,
|
|
24
|
+
type FC,
|
|
25
|
+
useMemo,
|
|
26
|
+
useState,
|
|
27
|
+
} from "react";
|
|
28
|
+
|
|
29
|
+
interface CheckboxProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
30
|
+
theme: Theme;
|
|
31
|
+
checked: boolean;
|
|
32
|
+
// biome-ignore lint/suspicious/noExplicitAny: don't know what it will be
|
|
33
|
+
onChange: (event: any) => Promise<void>;
|
|
34
|
+
labelText: string;
|
|
35
|
+
error?: string;
|
|
36
|
+
loading: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const checkboxBefore = css`{
|
|
40
|
+
&:before {
|
|
41
|
+
content: '""';
|
|
42
|
+
position: absolute;
|
|
43
|
+
height: 100%;
|
|
44
|
+
width: 100%;
|
|
45
|
+
}
|
|
46
|
+
}`;
|
|
47
|
+
|
|
48
|
+
const baseStyle: CSSProperties = {
|
|
49
|
+
width: "28px",
|
|
50
|
+
height: "28px",
|
|
51
|
+
minWidth: "14px",
|
|
52
|
+
minHeight: "14px",
|
|
53
|
+
top: "auto",
|
|
54
|
+
left: "auto",
|
|
55
|
+
opacity: "1",
|
|
56
|
+
borderRadius: "12.5%",
|
|
57
|
+
appearance: "none",
|
|
58
|
+
cursor: "pointer",
|
|
59
|
+
margin: "0",
|
|
60
|
+
borderStyle: "solid",
|
|
61
|
+
borderWidth: "1px",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const ID_LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
65
|
+
|
|
66
|
+
const FAQ_LINK = process.env.PROSOPO_DOCS_URL
|
|
67
|
+
? `${new URL(`${process.env.PROSOPO_DOCS_URL}/en/basics/faq/`).href}/`
|
|
68
|
+
: "https://docs.prosopo.io/en/basics/faq/";
|
|
69
|
+
|
|
70
|
+
const generateRandomId = () => {
|
|
71
|
+
return Array.from(
|
|
72
|
+
{ length: 8 },
|
|
73
|
+
() => ID_LETTERS[Math.floor(Math.random() * ID_LETTERS.length)],
|
|
74
|
+
).join("");
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
interface ResponsiveLabelProps {
|
|
78
|
+
htmFor?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const Checkbox: FC<CheckboxProps> = ({
|
|
82
|
+
theme,
|
|
83
|
+
onChange,
|
|
84
|
+
checked,
|
|
85
|
+
labelText,
|
|
86
|
+
error,
|
|
87
|
+
loading,
|
|
88
|
+
}: CheckboxProps) => {
|
|
89
|
+
const checkboxStyleBase: CSSProperties = {
|
|
90
|
+
...baseStyle,
|
|
91
|
+
border: `1px solid ${theme.palette.background.contrastText}`,
|
|
92
|
+
};
|
|
93
|
+
const [hover, setHover] = useState(false);
|
|
94
|
+
|
|
95
|
+
const ResponsiveLabel = styled.label<ResponsiveLabelProps>`
|
|
96
|
+
color: ${theme.palette.background.contrastText};
|
|
97
|
+
position: relative;
|
|
98
|
+
display: flex !important;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
user-select: none;
|
|
101
|
+
font-weight: normal;
|
|
102
|
+
font-family: ${theme.font.fontFamily};
|
|
103
|
+
@container prosopo-widget (max-width: 169px) {
|
|
104
|
+
display: none;
|
|
105
|
+
}
|
|
106
|
+
@container prosopo-widget (min-width: 170px) {
|
|
107
|
+
font-size: 10px;
|
|
108
|
+
}
|
|
109
|
+
@container prosopo-widget (min-width: 220px) {
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
}
|
|
112
|
+
@container prosopo-widget (min-width: 250px) {
|
|
113
|
+
font-size: 14px;
|
|
114
|
+
}
|
|
115
|
+
@container prosopo-widget (min-width: 270px) {
|
|
116
|
+
font-size: 16px;
|
|
117
|
+
}
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO fix
|
|
121
|
+
const checkboxStyle: CSSProperties = useMemo(() => {
|
|
122
|
+
return {
|
|
123
|
+
...checkboxStyleBase,
|
|
124
|
+
borderColor: hover
|
|
125
|
+
? theme.palette.background.contrastText
|
|
126
|
+
: theme.palette.border,
|
|
127
|
+
appearance: checked ? "auto" : "none",
|
|
128
|
+
flex: 1,
|
|
129
|
+
margin: "15px",
|
|
130
|
+
minWidth: "28px",
|
|
131
|
+
minHeight: "28px",
|
|
132
|
+
};
|
|
133
|
+
}, [hover, theme, checked]);
|
|
134
|
+
const id = generateRandomId();
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<span
|
|
138
|
+
style={{
|
|
139
|
+
display: "inline-flex",
|
|
140
|
+
alignItems: "center",
|
|
141
|
+
minHeight: "58px",
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
{loading ? (
|
|
145
|
+
<div
|
|
146
|
+
className={WIDGET_CHECKBOX_SPINNER_CSS_CLASS}
|
|
147
|
+
aria-label="Loading spinner"
|
|
148
|
+
/>
|
|
149
|
+
) : (
|
|
150
|
+
<input
|
|
151
|
+
name={id}
|
|
152
|
+
id={id}
|
|
153
|
+
onMouseEnter={() => setHover(true)}
|
|
154
|
+
onMouseLeave={() => setHover(false)}
|
|
155
|
+
css={checkboxBefore}
|
|
156
|
+
type={"checkbox"}
|
|
157
|
+
aria-live={"assertive"}
|
|
158
|
+
aria-label={labelText}
|
|
159
|
+
onKeyDown={(e) => {
|
|
160
|
+
if (!e.isTrusted) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (e.key === "Enter") {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
e.stopPropagation();
|
|
166
|
+
setHover(false);
|
|
167
|
+
onChange(e);
|
|
168
|
+
}
|
|
169
|
+
}}
|
|
170
|
+
onChange={(e) => {
|
|
171
|
+
if (!e.isTrusted) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
e.stopPropagation();
|
|
176
|
+
setHover(false);
|
|
177
|
+
onChange(e);
|
|
178
|
+
}}
|
|
179
|
+
checked={checked}
|
|
180
|
+
style={checkboxStyle}
|
|
181
|
+
disabled={error !== undefined}
|
|
182
|
+
className={loading ? "prosopo-checkbox__loading-spinner" : ""}
|
|
183
|
+
data-cy={"captcha-checkbox"}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
{error ? (
|
|
187
|
+
<ResponsiveLabel htmFor={id}>
|
|
188
|
+
<a
|
|
189
|
+
css={{
|
|
190
|
+
color: theme.palette.error.main,
|
|
191
|
+
}}
|
|
192
|
+
href={FAQ_LINK}
|
|
193
|
+
>
|
|
194
|
+
{error}
|
|
195
|
+
</a>
|
|
196
|
+
</ResponsiveLabel>
|
|
197
|
+
) : (
|
|
198
|
+
<ResponsiveLabel htmFor={id}>{labelText}</ResponsiveLabel>
|
|
199
|
+
)}
|
|
200
|
+
</span>
|
|
201
|
+
);
|
|
202
|
+
};
|
|
203
|
+
export default Checkbox;
|
|
@@ -0,0 +1,133 @@
|
|
|
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 {
|
|
16
|
+
type CSSProperties,
|
|
17
|
+
forwardRef,
|
|
18
|
+
useEffect,
|
|
19
|
+
useId,
|
|
20
|
+
useMemo,
|
|
21
|
+
useRef,
|
|
22
|
+
useState,
|
|
23
|
+
} from "react";
|
|
24
|
+
import { createPortal } from "react-dom";
|
|
25
|
+
|
|
26
|
+
interface HoneypotProps {
|
|
27
|
+
encodedQuestion: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const offscreenStyle: CSSProperties = {
|
|
31
|
+
position: "absolute",
|
|
32
|
+
left: "-9999px",
|
|
33
|
+
top: "-9999px",
|
|
34
|
+
width: "1px",
|
|
35
|
+
height: "1px",
|
|
36
|
+
overflow: "hidden",
|
|
37
|
+
opacity: 0,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Server wraps morse/semaphore in base64 (utf-8) for the wire. Strip the
|
|
41
|
+
// base64 layer here so the rendered label is the raw morse/semaphore an agent
|
|
42
|
+
// can recognise and engage with.
|
|
43
|
+
const decodeBase64Utf8 = (b64: string): string => {
|
|
44
|
+
const binary = atob(b64);
|
|
45
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
46
|
+
return new TextDecoder().decode(bytes);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Locate the dapp's enclosing <form> by stepping out of the widget's shadow
|
|
50
|
+
// root into light DOM, then walking up via closest(). Returns null when the
|
|
51
|
+
// widget isn't embedded inside a form.
|
|
52
|
+
const findAncestorForm = (anchor: Element): HTMLFormElement | null => {
|
|
53
|
+
const root = anchor.getRootNode();
|
|
54
|
+
const lightDomEntry = root instanceof ShadowRoot ? root.host : anchor;
|
|
55
|
+
return lightDomEntry instanceof Element
|
|
56
|
+
? lightDomEntry.closest("form")
|
|
57
|
+
: null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Honeypot must live in light DOM, not in the widget's shadow root: if it
|
|
61
|
+
// rendered there a bot would have to traverse `.shadowRoot` to reach it, and
|
|
62
|
+
// @prosopo/catcher patches that getter to detect (and restart on) automated
|
|
63
|
+
// access — wiping the value the bot just wrote before it can submit.
|
|
64
|
+
//
|
|
65
|
+
// Within light DOM we prefer the enclosing <form> so bots scraping
|
|
66
|
+
// `form.querySelectorAll('input')` discover the bait naturally; document.body
|
|
67
|
+
// is the fallback for widgets mounted outside any form.
|
|
68
|
+
//
|
|
69
|
+
// The decoded question is rendered as the input's <label>, not as its value.
|
|
70
|
+
// Naive form-fillers leave the empty input alone — no signal, no false
|
|
71
|
+
// positives. Agents that read the DOM as a prompt may decode the label and
|
|
72
|
+
// write an answer into the empty field; that response rides up as
|
|
73
|
+
// clientMetaData.hp.
|
|
74
|
+
export const Honeypot = forwardRef<HTMLInputElement, HoneypotProps>(
|
|
75
|
+
({ encodedQuestion }, ref) => {
|
|
76
|
+
const id = useId();
|
|
77
|
+
// Opaque non-existent form id. Setting `form="..."` on an input with a
|
|
78
|
+
// value that doesn't match any form's id disassociates the input from
|
|
79
|
+
// every form: the browser excludes it from the parent form's submission
|
|
80
|
+
// set and from `form.elements`, while leaving it discoverable via
|
|
81
|
+
// `form.querySelectorAll('input')`. Built from useId so it's unique per
|
|
82
|
+
// Honeypot instance and guaranteed not to collide with the input's own id.
|
|
83
|
+
const detachedFormId = `${id}-d`;
|
|
84
|
+
const question = useMemo(
|
|
85
|
+
() => decodeBase64Utf8(encodedQuestion),
|
|
86
|
+
[encodedQuestion],
|
|
87
|
+
);
|
|
88
|
+
const anchorRef = useRef<HTMLSpanElement>(null);
|
|
89
|
+
const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const anchor = anchorRef.current;
|
|
93
|
+
if (!anchor) return;
|
|
94
|
+
setPortalTarget(findAncestorForm(anchor) ?? document.body);
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
if (typeof document === "undefined") return null;
|
|
98
|
+
|
|
99
|
+
// Transient anchor while we resolve the portal target. Sits in the React
|
|
100
|
+
// tree (inside the shadow root) just long enough for the effect above to
|
|
101
|
+
// walk out to light DOM and find the form.
|
|
102
|
+
if (!portalTarget) {
|
|
103
|
+
return (
|
|
104
|
+
<span ref={anchorRef} aria-hidden="true" style={{ display: "none" }} />
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Input is nested inside the label (implicit association) instead of
|
|
109
|
+
// htmlFor-linked — biome's noLabelWithoutControl rule only recognises
|
|
110
|
+
// the descendant form of association at static-analysis time.
|
|
111
|
+
return createPortal(
|
|
112
|
+
<div aria-hidden="true" style={offscreenStyle}>
|
|
113
|
+
<label>
|
|
114
|
+
{question}
|
|
115
|
+
<input
|
|
116
|
+
ref={ref}
|
|
117
|
+
id={id}
|
|
118
|
+
form={detachedFormId}
|
|
119
|
+
type="text"
|
|
120
|
+
name="email_confirm"
|
|
121
|
+
defaultValue=""
|
|
122
|
+
tabIndex={-1}
|
|
123
|
+
autoComplete="off"
|
|
124
|
+
aria-hidden="true"
|
|
125
|
+
/>
|
|
126
|
+
</label>
|
|
127
|
+
</div>,
|
|
128
|
+
portalTarget,
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
Honeypot.displayName = "Honeypot";
|
|
@@ -0,0 +1,99 @@
|
|
|
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 { darkTheme, lightTheme } from "@prosopo/widget-skeleton";
|
|
16
|
+
import { type ButtonHTMLAttributes, type FC, useMemo, useState } from "react";
|
|
17
|
+
|
|
18
|
+
interface ReloadButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
19
|
+
themeColor: "light" | "dark";
|
|
20
|
+
onReload: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const buttonStyleBase = {
|
|
24
|
+
border: "none",
|
|
25
|
+
paddingTop: "6px",
|
|
26
|
+
paddingBottom: "6px",
|
|
27
|
+
cursor: "pointer",
|
|
28
|
+
height: "39px",
|
|
29
|
+
width: "39px",
|
|
30
|
+
borderRadius: "50%",
|
|
31
|
+
display: "flex",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const ReloadButton: FC<ReloadButtonProps> = ({
|
|
35
|
+
themeColor,
|
|
36
|
+
onReload,
|
|
37
|
+
}: ReloadButtonProps) => {
|
|
38
|
+
const theme = useMemo(
|
|
39
|
+
() => (themeColor === "light" ? lightTheme : darkTheme),
|
|
40
|
+
[themeColor],
|
|
41
|
+
);
|
|
42
|
+
const [hover, setHover] = useState(false);
|
|
43
|
+
const buttonStyle = useMemo(() => {
|
|
44
|
+
const baseStyle = {
|
|
45
|
+
...buttonStyleBase,
|
|
46
|
+
backgroundColor: theme.palette.background.default,
|
|
47
|
+
color: hover
|
|
48
|
+
? theme.palette.primary.contrastText
|
|
49
|
+
: theme.palette.background.contrastText,
|
|
50
|
+
border: `1px solid ${theme.palette.grey[500]}`,
|
|
51
|
+
borderRadius: "50%",
|
|
52
|
+
transition: "background-color 0.3s",
|
|
53
|
+
boxShadow: `0px 1px 3px 0px ${theme.palette.grey[500]}`,
|
|
54
|
+
justifyContent: "center",
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
margin: "0 auto",
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
...baseStyle,
|
|
60
|
+
backgroundColor: hover
|
|
61
|
+
? theme.palette.grey[700]
|
|
62
|
+
: theme.palette.background.default,
|
|
63
|
+
};
|
|
64
|
+
}, [hover, theme]);
|
|
65
|
+
return (
|
|
66
|
+
<button
|
|
67
|
+
className="reload-button"
|
|
68
|
+
aria-label="Reload"
|
|
69
|
+
type="button"
|
|
70
|
+
style={buttonStyle}
|
|
71
|
+
onMouseEnter={() => setHover(true)}
|
|
72
|
+
onMouseLeave={() => setHover(false)}
|
|
73
|
+
onClick={(e) => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
onReload();
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<svg
|
|
79
|
+
width="16px"
|
|
80
|
+
height="16px"
|
|
81
|
+
viewBox="0 0 16 16"
|
|
82
|
+
version="1.1"
|
|
83
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
84
|
+
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
85
|
+
style={{ display: "flex" }}
|
|
86
|
+
>
|
|
87
|
+
<title>reload</title>
|
|
88
|
+
<path
|
|
89
|
+
shapeRendering="optimizeQuality"
|
|
90
|
+
fill={
|
|
91
|
+
hover ? theme.palette.primary.contrastText : theme.palette.grey[700]
|
|
92
|
+
}
|
|
93
|
+
transform={"scale(0.0416)"}
|
|
94
|
+
d="M234.666667,149.333333 L234.666667,106.666667 L314.564847,106.664112 C287.579138,67.9778918 242.745446,42.6666667 192,42.6666667 C109.525477,42.6666667 42.6666667,109.525477 42.6666667,192 C42.6666667,274.474523 109.525477,341.333333 192,341.333333 C268.201293,341.333333 331.072074,284.258623 340.195444,210.526102 L382.537159,215.817985 C370.807686,310.617565 289.973536,384 192,384 C85.961328,384 1.42108547e-14,298.038672 1.42108547e-14,192 C1.42108547e-14,85.961328 85.961328,1.42108547e-14 192,1.42108547e-14 C252.316171,1.42108547e-14 306.136355,27.8126321 341.335366,71.3127128 L341.333333,1.42108547e-14 L384,1.42108547e-14 L384,149.333333 L234.666667,149.333333 Z"
|
|
95
|
+
/>
|
|
96
|
+
</svg>
|
|
97
|
+
</button>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
/** @jsxImportSource @emotion/react */
|
|
16
|
+
|
|
17
|
+
import { TestSiteKeyMode, getTestSiteKeyMode } from "@prosopo/types";
|
|
18
|
+
import { type FC, useEffect } from "react";
|
|
19
|
+
|
|
20
|
+
interface TestModeBannerProps {
|
|
21
|
+
siteKey: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Renders a prominent warning when the widget is configured with one of the
|
|
25
|
+
// reserved CI test site keys (always-pass / always-fail). Renders nothing for a
|
|
26
|
+
// normal site key. This is the user-facing safeguard that stops a test key from
|
|
27
|
+
// being shipped to production unnoticed.
|
|
28
|
+
export const TestModeBanner: FC<TestModeBannerProps> = ({
|
|
29
|
+
siteKey,
|
|
30
|
+
}: TestModeBannerProps) => {
|
|
31
|
+
const mode: TestSiteKeyMode | null = getTestSiteKeyMode(siteKey);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (mode !== null) {
|
|
35
|
+
console.warn(
|
|
36
|
+
`[Procaptcha] WARNING: site key "${siteKey}" is a TEST key that ALWAYS ${
|
|
37
|
+
mode === TestSiteKeyMode.Pass ? "PASSES" : "FAILS"
|
|
38
|
+
}. Never use it in production.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}, [mode, siteKey]);
|
|
42
|
+
|
|
43
|
+
if (mode === null) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const action =
|
|
48
|
+
mode === TestSiteKeyMode.Pass ? "ALWAYS PASSES" : "ALWAYS FAILS";
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
// biome-ignore lint/a11y/useSemanticElements: the "alert" role has no native HTML element equivalent
|
|
52
|
+
<div
|
|
53
|
+
role="alert"
|
|
54
|
+
data-cy="test-mode-banner"
|
|
55
|
+
css={{
|
|
56
|
+
width: "100%",
|
|
57
|
+
boxSizing: "border-box",
|
|
58
|
+
padding: "6px 10px",
|
|
59
|
+
backgroundColor: "#fff3cd",
|
|
60
|
+
color: "#664d03",
|
|
61
|
+
border: "1px solid #ffe69c",
|
|
62
|
+
borderRadius: "4px",
|
|
63
|
+
fontSize: "11px",
|
|
64
|
+
lineHeight: "1.3",
|
|
65
|
+
fontFamily:
|
|
66
|
+
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
67
|
+
textAlign: "center",
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{`⚠ Test mode: this site key ${action}. Do not use in production.`}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default TestModeBanner;
|