@prosopo/procaptcha-puzzle 2.13.3 → 2.14.1

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.
Files changed (77) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +6 -6
  2. package/.turbo/turbo-build$colon$tsc.log +20 -20
  3. package/.turbo/turbo-build.log +7 -7
  4. package/CHANGELOG.md +66 -0
  5. package/dist/cjs/components/procaptchaPuzzle.cjs +22 -0
  6. package/dist/cjs/components/procaptchaWidget.cjs +225 -0
  7. package/dist/cjs/components/puzzleCanvas.cjs +451 -0
  8. package/dist/cjs/index.cjs +7 -2
  9. package/dist/components/procaptchaPuzzle.d.ts +7 -0
  10. package/dist/components/procaptchaPuzzle.d.ts.map +1 -0
  11. package/dist/components/procaptchaPuzzle.js +21 -0
  12. package/dist/components/procaptchaPuzzle.js.map +1 -0
  13. package/dist/components/procaptchaWidget.d.ts +7 -0
  14. package/dist/components/procaptchaWidget.d.ts.map +1 -0
  15. package/dist/components/procaptchaWidget.js +225 -0
  16. package/dist/components/procaptchaWidget.js.map +1 -0
  17. package/dist/components/{PuzzleCanvas.d.ts → puzzleCanvas.d.ts} +6 -4
  18. package/dist/components/puzzleCanvas.d.ts.map +1 -0
  19. package/dist/components/puzzleCanvas.js +451 -0
  20. package/dist/components/puzzleCanvas.js.map +1 -0
  21. package/dist/index.d.ts +3 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/tests/domHarness.d.ts +20 -0
  26. package/dist/tests/domHarness.d.ts.map +1 -0
  27. package/dist/tests/domHarness.js +56 -0
  28. package/dist/tests/domHarness.js.map +1 -0
  29. package/dist/tests/invisibleCanvasVisibility.unit.test.d.ts +2 -0
  30. package/dist/tests/invisibleCanvasVisibility.unit.test.d.ts.map +1 -0
  31. package/dist/tests/invisibleCanvasVisibility.unit.test.js +102 -0
  32. package/dist/tests/invisibleCanvasVisibility.unit.test.js.map +1 -0
  33. package/dist/tests/procaptchaPuzzle.test-d.js +13 -10
  34. package/dist/tests/procaptchaPuzzle.test-d.js.map +1 -1
  35. package/dist/tests/procaptchaPuzzle.unit.test.js +44 -33
  36. package/dist/tests/procaptchaPuzzle.unit.test.js.map +1 -1
  37. package/dist/tests/procaptchaWidget.unit.test.js +108 -175
  38. package/dist/tests/procaptchaWidget.unit.test.js.map +1 -1
  39. package/dist/tests/puzzleCanvas.unit.test.js +56 -82
  40. package/dist/tests/puzzleCanvas.unit.test.js.map +1 -1
  41. package/package.json +10 -14
  42. package/src/components/procaptchaPuzzle.ts +54 -0
  43. package/src/components/procaptchaWidget.ts +392 -0
  44. package/src/components/puzzleCanvas.ts +658 -0
  45. package/src/index.ts +3 -2
  46. package/src/tests/domHarness.ts +122 -0
  47. package/src/tests/invisibleCanvasVisibility.unit.test.ts +179 -0
  48. package/src/tests/procaptchaPuzzle.test-d.ts +33 -17
  49. package/src/tests/procaptchaPuzzle.unit.test.ts +59 -44
  50. package/src/tests/procaptchaWidget.unit.test.ts +145 -258
  51. package/src/tests/puzzleCanvas.unit.test.ts +94 -122
  52. package/tsconfig.cjs.json +1 -2
  53. package/tsconfig.json +1 -2
  54. package/tsconfig.tsbuildinfo +1 -1
  55. package/vite.test.config.ts +3 -4
  56. package/dist/cjs/components/ProcaptchaPuzzle.cjs +0 -7
  57. package/dist/cjs/components/ProcaptchaWidget.cjs +0 -206
  58. package/dist/cjs/components/PuzzleCanvas.cjs +0 -476
  59. package/dist/components/ProcaptchaPuzzle.d.ts +0 -3
  60. package/dist/components/ProcaptchaPuzzle.d.ts.map +0 -1
  61. package/dist/components/ProcaptchaPuzzle.js +0 -7
  62. package/dist/components/ProcaptchaPuzzle.js.map +0 -1
  63. package/dist/components/ProcaptchaWidget.d.ts +0 -4
  64. package/dist/components/ProcaptchaWidget.d.ts.map +0 -1
  65. package/dist/components/ProcaptchaWidget.js +0 -207
  66. package/dist/components/ProcaptchaWidget.js.map +0 -1
  67. package/dist/components/PuzzleCanvas.d.ts.map +0 -1
  68. package/dist/components/PuzzleCanvas.js +0 -476
  69. package/dist/components/PuzzleCanvas.js.map +0 -1
  70. package/dist/tests/setup.d.ts +0 -5
  71. package/dist/tests/setup.d.ts.map +0 -1
  72. package/dist/tests/setup.js +0 -3
  73. package/dist/tests/setup.js.map +0 -1
  74. package/src/components/ProcaptchaPuzzle.tsx +0 -36
  75. package/src/components/ProcaptchaWidget.tsx +0 -320
  76. package/src/components/PuzzleCanvas.tsx +0 -638
  77. package/src/tests/setup.ts +0 -26
@@ -1,320 +0,0 @@
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, useTranslation } from "@prosopo/locale";
16
- import { buildUpdateState, useProcaptcha } from "@prosopo/procaptcha-common";
17
- import { Checkbox, Honeypot, isEventTrusted } from "@prosopo/procaptcha-common";
18
- import {
19
- type GetPuzzleCaptchaResponse,
20
- ModeEnum,
21
- type ProcaptchaProps,
22
- type PuzzleEvent,
23
- } from "@prosopo/types";
24
- import { darkTheme, lightTheme } from "@prosopo/widget-skeleton";
25
- import { useCallback, useEffect, useRef, useState } from "react";
26
- import { Manager } from "../services/Manager.js";
27
- import { PuzzleCanvas } from "./PuzzleCanvas.js";
28
-
29
- // Define the same event name as in the bundle for consistency
30
- const PROCAPTCHA_EXECUTE_EVENT = "procaptcha:execute";
31
-
32
- type PuzzlePhase = "checkbox" | "dragging" | "submitting";
33
-
34
- const Procaptcha = (props: ProcaptchaProps) => {
35
- const { t, ready: isTranslationReady } = useTranslation();
36
- const config = props.config;
37
- const i18n = props.i18n;
38
- const theme = "light" === config.theme ? lightTheme : darkTheme;
39
- const frictionlessState = props.frictionlessState; // Set up Session ID and Provider if they exist
40
- const callbacks = props.callbacks || {};
41
- const [state, _updateState] = useProcaptcha(useState, useRef);
42
- const [loading, setLoading] = useState(false);
43
- const [puzzlePhase, setPuzzlePhase] = useState<PuzzlePhase>("checkbox");
44
- const [challengeData, setChallengeData] =
45
- useState<GetPuzzleCaptchaResponse | null>(null);
46
- const [showRetry, setShowRetry] = useState(false);
47
- // get the state update mechanism
48
- const updateState = buildUpdateState(state, _updateState);
49
- const hpRef = useRef<HTMLInputElement>(null);
50
- const manager = useRef(
51
- Manager(
52
- config,
53
- state,
54
- updateState,
55
- callbacks,
56
- frictionlessState,
57
- () => hpRef.current?.value || undefined,
58
- ),
59
- );
60
- // See ProcaptchaWidget (procaptcha-pow) — same session-invalidation
61
- // recovery contract with coords preservation across a re-mint.
62
- const lastCoordsRef = useRef<{ x: number; y: number } | null>(null);
63
- const sessionInvalidatedFiredRef = useRef(false);
64
-
65
- useEffect(() => {
66
- if (!config.language) return;
67
- if (i18n) {
68
- if (i18n.language !== config.language) {
69
- void i18n.changeLanguage(config.language);
70
- }
71
- return;
72
- }
73
- // Direct-React consumers don't go through WidgetFactory, so pass the
74
- // language into loadI18next — first init boots with the right language
75
- // (skipping browser detection), and subsequent calls reconcile via
76
- // changeLanguage inside loadI18next.
77
- void loadI18next(false, config.language);
78
- }, [i18n, config.language]);
79
-
80
- useEffect(() => {
81
- if (!props.autoStart) return;
82
- setLoading(true);
83
- setShowRetry(false);
84
- const coords = props.startCoords;
85
- lastCoordsRef.current = coords ?? null;
86
- manager.current.start(coords?.x ?? 0, coords?.y ?? 0).then(
87
- (challenge) => {
88
- if (challenge) {
89
- setChallengeData(challenge);
90
- setPuzzlePhase("dragging");
91
- }
92
- setLoading(false);
93
- },
94
- () => setLoading(false),
95
- );
96
- }, [props.autoStart, props.startCoords]);
97
-
98
- useEffect(() => {
99
- if (!state.error) return undefined;
100
- setLoading(false);
101
- setPuzzlePhase("checkbox");
102
- setChallengeData(null);
103
- setShowRetry(false);
104
- if (state.error.key !== "CAPTCHA.NO_SESSION_FOUND") return undefined;
105
- if (props.onSessionInvalidated && !sessionInvalidatedFiredRef.current) {
106
- sessionInvalidatedFiredRef.current = true;
107
- const coords = lastCoordsRef.current;
108
- props.onSessionInvalidated(coords?.x, coords?.y);
109
- return undefined;
110
- }
111
- if (frictionlessState) {
112
- const timer = setTimeout(() => {
113
- frictionlessState.restart();
114
- }, 100);
115
- return () => clearTimeout(timer);
116
- }
117
- return undefined;
118
- }, [state.error, frictionlessState, props.onSessionInvalidated]);
119
-
120
- // A bare execute() reaches every invisible widget via document. A targeted
121
- // execute() is dispatched on this widget's container and works in either
122
- // mode, which is what lets a bound button drive a visible widget.
123
- useEffect(() => {
124
- const handleExecuteEvent = async () => {
125
- if (loading) {
126
- return;
127
- }
128
- setLoading(true);
129
- setShowRetry(false);
130
- try {
131
- const challenge = await manager.current.start();
132
- if (challenge) {
133
- setChallengeData(challenge);
134
- setPuzzlePhase("dragging");
135
- }
136
- } catch (error) {
137
- callbacks.onError?.(
138
- error instanceof Error ? error : new Error(String(error)),
139
- );
140
- } finally {
141
- setLoading(false);
142
- }
143
- };
144
-
145
- const container = props.container;
146
- const invisible = config.mode === ModeEnum.invisible;
147
- container?.addEventListener(PROCAPTCHA_EXECUTE_EVENT, handleExecuteEvent);
148
- if (invisible) {
149
- document.addEventListener(PROCAPTCHA_EXECUTE_EVENT, handleExecuteEvent);
150
- }
151
-
152
- return () => {
153
- container?.removeEventListener(
154
- PROCAPTCHA_EXECUTE_EVENT,
155
- handleExecuteEvent,
156
- );
157
- if (invisible) {
158
- document.removeEventListener(
159
- PROCAPTCHA_EXECUTE_EVENT,
160
- handleExecuteEvent,
161
- );
162
- }
163
- };
164
- }, [config.mode, callbacks.onError, loading, props.container]);
165
-
166
- const handlePuzzleComplete = useCallback(
167
- async (finalX: number, finalY: number, puzzleEvents: PuzzleEvent[]) => {
168
- setPuzzlePhase("submitting");
169
- let verified = false;
170
- try {
171
- verified = await manager.current.submitSolution(
172
- finalX,
173
- finalY,
174
- puzzleEvents,
175
- );
176
- } catch (error) {
177
- callbacks.onError?.(
178
- error instanceof Error ? error : new Error(String(error)),
179
- );
180
- }
181
-
182
- if (verified) {
183
- setPuzzlePhase("checkbox");
184
- setChallengeData(null);
185
- setShowRetry(false);
186
- setLoading(false);
187
- return;
188
- }
189
-
190
- // Failed — show retry message and fetch a new challenge
191
- setShowRetry(true);
192
- setPuzzlePhase("dragging");
193
-
194
- try {
195
- const newChallenge = await manager.current.start();
196
- if (newChallenge) {
197
- setChallengeData(newChallenge);
198
- } else {
199
- // Couldn't get new challenge, fall back to checkbox
200
- setPuzzlePhase("checkbox");
201
- setChallengeData(null);
202
- setShowRetry(false);
203
- }
204
- } catch {
205
- setPuzzlePhase("checkbox");
206
- setChallengeData(null);
207
- setShowRetry(false);
208
- }
209
- setLoading(false);
210
- },
211
- [callbacks.onError],
212
- );
213
-
214
- const isInvisible = config.mode === ModeEnum.invisible;
215
-
216
- // Dismissing returns to the checkbox; clicking away is not a wrong answer.
217
- const handleDismiss = useCallback(() => {
218
- setPuzzlePhase("checkbox");
219
- setChallengeData(null);
220
- setShowRetry(false);
221
- setLoading(false);
222
- }, []);
223
- const showPuzzleOverlay =
224
- (puzzlePhase === "dragging" || puzzlePhase === "submitting") &&
225
- challengeData;
226
-
227
- return (
228
- <>
229
- {frictionlessState?.hp && (
230
- <Honeypot ref={hpRef} encodedQuestion={frictionlessState.hp} />
231
- )}
232
- {/* Puzzle overlay — shown in both visible and invisible modes once a
233
- challenge has been fetched; puzzle is inherently interactive. */}
234
- {showPuzzleOverlay && (
235
- <PuzzleCanvas
236
- originX={challengeData.originX}
237
- originY={challengeData.originY}
238
- background={challengeData.background}
239
- piece={challengeData.piece}
240
- pieceSize={challengeData.pieceSize}
241
- onComplete={handlePuzzleComplete}
242
- showRetry={showRetry}
243
- submitting={puzzlePhase === "submitting"}
244
- theme={theme}
245
- placement={config.placement}
246
- anchor={props.container}
247
- onDismiss={handleDismiss}
248
- />
249
- )}
250
-
251
- {/* Checkbox — only in visible mode. Invisible mode is driven by
252
- the host page's execute() call (e.g. on form submit). */}
253
- {!isInvisible && (
254
- <Checkbox
255
- checked={state.isHuman}
256
- theme={theme}
257
- onChange={async (event: React.MouseEvent | React.TouchEvent) => {
258
- if (loading) {
259
- return;
260
- }
261
- setLoading(true);
262
- setShowRetry(false);
263
-
264
- // Capture click coordinates (mirrors POW widget) so the
265
- // puzzle solution salt records the entry-point telemetry.
266
- let x = 0;
267
- let y = 0;
268
- const mouseOrTouchEvent = event.nativeEvent;
269
- if (!isEventTrusted(mouseOrTouchEvent)) {
270
- // Don't capture coordinates for non-trusted events
271
- } else if (
272
- "touches" in mouseOrTouchEvent &&
273
- mouseOrTouchEvent.touches.length > 0 &&
274
- mouseOrTouchEvent.touches[0]
275
- ) {
276
- x = mouseOrTouchEvent.touches[0].clientX;
277
- y = mouseOrTouchEvent.touches[0].clientY;
278
- } else if (
279
- "clientX" in mouseOrTouchEvent &&
280
- "clientY" in mouseOrTouchEvent
281
- ) {
282
- x = mouseOrTouchEvent.clientX;
283
- y = mouseOrTouchEvent.clientY;
284
- }
285
-
286
- lastCoordsRef.current = { x, y };
287
- try {
288
- const challenge = await manager.current.start(x, y);
289
-
290
- if (challenge) {
291
- setChallengeData(challenge);
292
- setPuzzlePhase("dragging");
293
- }
294
- } catch (error) {
295
- // The manager reports failures through state.error;
296
- // rethrowing here only produces an unhandled rejection,
297
- // since nothing awaits this handler.
298
- callbacks.onError?.(
299
- error instanceof Error ? error : new Error(String(error)),
300
- );
301
- } finally {
302
- // A rejected start would otherwise leave the spinner up for
303
- // good, with no way back to the checkbox for the user.
304
- setLoading(false);
305
- }
306
- }}
307
- labelText={isTranslationReady ? t("WIDGET.I_AM_HUMAN") : ""}
308
- error={state.error?.message}
309
- aria-label="human checkbox"
310
- loadingText={t("WIDGET.CHECKING", {
311
- defaultValue: "Checking that you are human",
312
- })}
313
- loading={loading || puzzlePhase === "submitting"}
314
- />
315
- )}
316
- </>
317
- );
318
- };
319
-
320
- export default Procaptcha;