@prosopo/procaptcha-puzzle 2.15.0 → 2.17.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.
@@ -43,10 +43,10 @@ Bundle build started
43
43
  ✓ 6 modules transformed.
44
44
  rendering chunks...
45
45
  computing gzip size...
46
+ dist/cjs/components/procaptchaPuzzle.cjs 0.47 kB │ gzip: 0.22 kB
46
47
  dist/cjs/index.cjs 0.60 kB │ gzip: 0.21 kB
47
- dist/cjs/components/procaptchaPuzzle.cjs 0.74 kB │ gzip: 0.38 kB
48
- dist/cjs/components/procaptchaWidget.cjs 8.32 kB │ gzip: 2.28 kB
49
- dist/cjs/services/Manager.cjs 10.16 kB │ gzip: 2.82 kB
50
- dist/cjs/components/puzzleCanvas.cjs 14.77 kB │ gzip: 4.39 kB
48
+ dist/cjs/services/Manager.cjs 8.66 kB │ gzip: 2.45 kB
49
+ dist/cjs/components/procaptchaWidget.cjs 8.86 kB │ gzip: 2.40 kB
50
+ dist/cjs/components/puzzleCanvas.cjs 16.87 kB │ gzip: 4.79 kB
51
51
 
52
- ✓ built in 61ms
52
+ ✓ built in 85ms
@@ -1,5 +1,5 @@
1
1
  $ tsc --build --verbose
2
- 1:37:59 PM - Projects in this build:
2
+ 11:33:49 AM - Projects in this build:
3
3
  * ../../dev/config/tsconfig.json
4
4
  * ../util/tsconfig.json
5
5
  * ../locale/tsconfig.json
@@ -17,37 +17,37 @@ $ tsc --build --verbose
17
17
  * ../procaptcha-common/tsconfig.json
18
18
  * tsconfig.json
19
19
 
20
- 1:37:59 PM - Project '../../dev/config/tsconfig.json' is up to date because newest input '../../dev/config/src/webpack/webpack.config.ts' is older than output '../../dev/config/tsconfig.tsbuildinfo'
20
+ 11:33:49 AM - Project '../../dev/config/tsconfig.json' is up to date because newest input '../../dev/config/src/webpack/webpack.config.ts' is older than output '../../dev/config/tsconfig.tsbuildinfo'
21
21
 
22
- 1:37:59 PM - Project '../util/tsconfig.json' is up to date because newest input '../util/src/url.ts' is older than output '../util/tsconfig.tsbuildinfo'
22
+ 11:33:49 AM - Project '../util/tsconfig.json' is up to date because newest input '../util/src/url.ts' is older than output '../util/tsconfig.tsbuildinfo'
23
23
 
24
- 1:37:59 PM - Project '../locale/tsconfig.json' is up to date because newest input '../locale/src/translationKey.ts' is older than output '../locale/tsconfig.tsbuildinfo'
24
+ 11:33:49 AM - Project '../locale/tsconfig.json' is up to date because newest input '../locale/src/translationKey.ts' is older than output '../locale/tsconfig.tsbuildinfo'
25
25
 
26
- 1:37:59 PM - Project '../util-crypto/tsconfig.json' is up to date because newest input '../util-crypto/src/types.ts' is older than output '../util-crypto/tsconfig.tsbuildinfo'
26
+ 11:33:49 AM - Project '../util-crypto/tsconfig.json' is up to date because newest input '../util-crypto/src/types.ts' is older than output '../util-crypto/tsconfig.tsbuildinfo'
27
27
 
28
- 1:37:59 PM - Project '../types/tsconfig.json' is up to date because newest input '../types/src/types.test-d.ts' is older than output '../types/tsconfig.tsbuildinfo'
28
+ 11:33:49 AM - Project '../types/tsconfig.json' is up to date because newest input '../types/src/types.test-d.ts' is older than output '../types/tsconfig.tsbuildinfo'
29
29
 
30
- 1:37:59 PM - Project '../api/tsconfig.json' is up to date because newest input '../api/src/tests/apiHarness.ts' is older than output '../api/tsconfig.tsbuildinfo'
30
+ 11:33:49 AM - Project '../api/tsconfig.json' is up to date because newest input '../api/src/tests/apiHarness.ts' is older than output '../api/tsconfig.tsbuildinfo'
31
31
 
32
- 1:37:59 PM - Project '../logger/tsconfig.json' is up to date because newest input '../logger/src/index.ts' is older than output '../logger/tsconfig.tsbuildinfo'
32
+ 11:33:49 AM - Project '../logger/tsconfig.json' is up to date because newest input '../logger/src/index.ts' is older than output '../logger/tsconfig.tsbuildinfo'
33
33
 
34
- 1:37:59 PM - Project '../common/tsconfig.json' is up to date because newest input '../common/src/index.ts' is older than output '../common/tsconfig.tsbuildinfo'
34
+ 11:33:49 AM - Project '../common/tsconfig.json' is up to date because newest input '../common/src/error.ts' is older than output '../common/tsconfig.tsbuildinfo'
35
35
 
36
- 1:37:59 PM - Project '../load-balancer/tsconfig.json' is up to date because newest input '../load-balancer/src/developmentProviderUrl.ts' is older than output '../load-balancer/tsconfig.tsbuildinfo'
36
+ 11:33:50 AM - Project '../load-balancer/tsconfig.json' is up to date because newest input '../load-balancer/src/balancer.ts' is older than output '../load-balancer/tsconfig.tsbuildinfo'
37
37
 
38
- 1:37:59 PM - Project '../widget-skeleton/tsconfig.json' is up to date because newest input '../widget-skeleton/src/theme.ts' is older than output '../widget-skeleton/tsconfig.tsbuildinfo'
38
+ 11:33:50 AM - Project '../widget-skeleton/tsconfig.json' is up to date because newest input '../widget-skeleton/src/webComponent/createWidget.ts' is older than output '../widget-skeleton/tsconfig.tsbuildinfo'
39
39
 
40
- 1:37:59 PM - Project '../fingerprintjs/tsconfig.json' is up to date because newest input '../fingerprintjs/src/utils/browser.ts' is older than output '../fingerprintjs/tsconfig.tsbuildinfo'
40
+ 11:33:50 AM - Project '../fingerprintjs/tsconfig.json' is up to date because newest input '../fingerprintjs/src/utils/browser.ts' is older than output '../fingerprintjs/tsconfig.tsbuildinfo'
41
41
 
42
- 1:37:59 PM - Project '../fingerprint/tsconfig.json' is up to date because newest input '../fingerprint/src/index.ts' is older than output '../fingerprint/tsconfig.tsbuildinfo'
42
+ 11:33:50 AM - Project '../fingerprint/tsconfig.json' is up to date because newest input '../fingerprint/src/index.ts' is older than output '../fingerprint/tsconfig.tsbuildinfo'
43
43
 
44
- 1:37:59 PM - Project '../keyring/tsconfig.json' is up to date because newest input '../keyring/src/pair/decode.ts' is older than output '../keyring/tsconfig.tsbuildinfo'
44
+ 11:33:50 AM - Project '../keyring/tsconfig.json' is up to date because newest input '../keyring/src/pair/decode.ts' is older than output '../keyring/tsconfig.tsbuildinfo'
45
45
 
46
- 1:37:59 PM - Project '../account/tsconfig.json' is up to date because newest input '../account/src/custom.d.ts' is older than output '../account/tsconfig.tsbuildinfo'
46
+ 11:33:50 AM - Project '../account/tsconfig.json' is up to date because newest input '../account/src/custom.d.ts' is older than output '../account/tsconfig.tsbuildinfo'
47
47
 
48
- 1:37:59 PM - Project '../procaptcha-common/tsconfig.json' is up to date because newest input '../procaptcha-common/src/extensionLoader.ts' is older than output '../procaptcha-common/tsconfig.tsbuildinfo'
48
+ 11:33:50 AM - Project '../procaptcha-common/tsconfig.json' is up to date because newest input '../procaptcha-common/src/state/managerLifecycle.ts' is older than output '../procaptcha-common/tsconfig.tsbuildinfo'
49
49
 
50
- 1:37:59 PM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
50
+ 11:33:50 AM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
51
51
 
52
- 1:37:59 PM - Building project '/home/runner/work/captcha/captcha/packages/procaptcha-puzzle/tsconfig.json'...
52
+ 11:33:50 AM - Building project '/home/runner/work/captcha/captcha/packages/procaptcha-puzzle/tsconfig.json'...
53
53
 
@@ -45,10 +45,10 @@ Bundle build started
45
45
  rendering chunks...
46
46
  computing gzip size...
47
47
  dist/_virtual/_rolldown/runtime.js 0.04 kB │ gzip: 0.06 kB
48
+ dist/components/procaptchaPuzzle.js 0.33 kB │ gzip: 0.19 kB
48
49
  dist/index.js 0.38 kB │ gzip: 0.16 kB
49
- dist/components/procaptchaPuzzle.js 0.65 kB │ gzip: 0.36 kB
50
- dist/components/procaptchaWidget.js 7.98 kB │ gzip: 2.28 kB
51
- dist/services/Manager.js 9.62 kB │ gzip: 2.78 kB
52
- dist/components/puzzleCanvas.js 14.04 kB │ gzip: 4.36 kB
50
+ dist/services/Manager.js 8.12 kB │ gzip: 2.41 kB
51
+ dist/components/procaptchaWidget.js 8.49 kB │ gzip: 2.40 kB
52
+ dist/components/puzzleCanvas.js 16.04 kB │ gzip: 4.78 kB
53
53
 
54
- ✓ built in 72ms
54
+ ✓ built in 78ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # @prosopo/procaptcha-puzzle
2
2
 
3
+ ## 2.17.0
4
+ ### Minor Changes
5
+
6
+ - 4570692: Let a user swap the puzzle captcha for an image challenge, on sites where Prosopo has switched it on.
7
+
8
+ Sites get a new staff-only setting, `widgetFeatureFlags.puzzleImageSwitch`. It is off by default and has no stored default. When it is on and the site allows image challenges, the puzzle response says `imageSwitchAvailable: true`. The puzzle then shows a small grid icon next to its title, with a "Switch to an image challenge" tooltip on hover or keyboard focus. After two wrong answers or refreshes in a row, the icon fills with the primary colour and its tooltip shows without a hover, because a touch screen has no hover. The tooltip goes away once the user starts moving the piece.
9
+
10
+ Pressing it re-mints the session the same way refresh does, with `switchToImage: true`. The provider only honours that when the request is a refresh of a puzzle session on the same site and the switch is on. The new session is then an image challenge with reason `PUZZLE_USER_SWITCH`. Like the refresh limit, it only ever moves from puzzle to image, so a client that fakes the request can only make its own challenge harder.
11
+
12
+ Also fixes a wrong puzzle answer restarting the whole widget. The user was sent back to the checkbox instead of getting a new puzzle with "Not quite — try again", and a wasted `/frictionless` request went out. The puzzle now retries in place.
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies [4570692]
17
+ - @prosopo/procaptcha-common@2.21.0
18
+ - @prosopo/api@4.6.0
19
+ - @prosopo/types@5.15.0
20
+ - @prosopo/locale@3.9.0
21
+ - @prosopo/common@3.1.64
22
+
23
+ ## 2.16.0
24
+ ### Minor Changes
25
+
26
+ - b299a91: An audio challenge, offered only as an accessibility alternative, like reCAPTCHA's audio option. The user hears a short sequence of spoken digits and types them in.
27
+
28
+ It is off by default and needs two switches. Prosopo has to turn on the `captchaTypeFeatureFlags.audio` feature flag for the site, which the site owner cannot do, and the site owner has to set `audioAccessibilityEnabled`. When both are on, image, puzzle and icon-order challenges show a "Use audio instead" button. Pressing it swaps the visual challenge for the audio one. After a wrong answer the user stays on audio and gets a fresh clip.
29
+
30
+ Audio is never a captcha type that can be selected or routed to. A site's `captchaType`, traffic-filter categories, Restrict rules and the site-key CLI all reject it, and routing, PoW escalation and the severity tiers leave it out. The provider only serves audio against the visual session the user was already given, and only on a site with both switches on; any other audio request is refused before the session is used up, and a request without a session is always refused.
31
+
32
+ The spoken digits are synthesised by `@prosopo/audio-assets`, so there is no recorded set of clips to collect. The answer never leaves the provider, and a challenge can be submitted and verified only once, even under concurrent requests. `@prosopo/procaptcha-audio` is the widget. It and the "Use audio instead" button are built on the shared widget code in `@prosopo/procaptcha-common`, and the provider side is built on the shared interactive-captcha code that puzzle and icon-order use.
33
+
34
+ The demo playground has audio pages, and there is an end-to-end test for it.
35
+
36
+ ### Patch Changes
37
+
38
+ - e13d7a8: New captcha type: `iconOrder`. The user is shown a frame of icons and a legend, and clicks the legend's icons in the order given.
39
+
40
+ Icon-order is off by default. Only Prosopo can switch it on for a site, with the `captchaTypeFeatureFlags.iconOrder` feature flag; the site owner cannot. A site without the flag is never served icon-order by any route, and the challenge endpoint refuses it. Once the flag is on, the owner can still keep icon-order out of the frictionless flow with `frictionlessTypes.iconOrder`, the same way as image and puzzle.
41
+
42
+ The answer never leaves the provider. Icon positions are stored on the challenge record, and the widget receives only the rendered frame and legend. Grading checks order as well as position, and each icon's hit radius scales with its size. Verifying a token is single-use under concurrent requests.
43
+
44
+ `@prosopo/icon-order-assets` draws the imagery, and `@prosopo/procaptcha-icon-order` is the widget. Its text is translated into every supported language.
45
+
46
+ Puzzle and icon-order now share their server code: challenge and solution handlers, the verify route, the submit and verify pipeline, and the database record methods. The widget code they have in common moves into `@prosopo/procaptcha-common`: the lazy mount wrapper, manager expiry and dispose, spent-session handling, behavioural data encryption and trusted click coordinates. Puzzle's behaviour is unchanged.
47
+
48
+ The demo playground has icon-order pages, and there is an end-to-end test for it.
49
+ - Updated dependencies [b299a91]
50
+ - Updated dependencies [e13d7a8]
51
+ - Updated dependencies [270395d]
52
+ - @prosopo/types@5.14.0
53
+ - @prosopo/api@4.5.0
54
+ - @prosopo/procaptcha-common@2.20.0
55
+ - @prosopo/locale@3.8.0
56
+ - @prosopo/common@3.1.63
57
+
3
58
  ## 2.15.0
4
59
  ### Minor Changes
5
60
 
@@ -1,22 +1,7 @@
1
+ let _prosopo_procaptcha_common = require("@prosopo/procaptcha-common");
1
2
  //#region src/components/procaptchaPuzzle.ts
2
- /**
3
- * Dynamic import so the puzzle widget lands in its own chunk, replacing the
4
- * `lazy()` + `<Suspense>` pair that used to provide the split.
5
- */
6
3
  var loadProcaptchaPuzzle = async () => (await Promise.resolve().then(() => require("./procaptchaWidget.cjs"))).mountProcaptchaPuzzleWidget;
7
- var mountProcaptchaPuzzle = (container, props) => {
8
- let destroyed = false;
9
- let inner;
10
- loadProcaptchaPuzzle().then((mount) => {
11
- if (destroyed) return;
12
- inner = mount(container, props);
13
- });
14
- return { destroy: () => {
15
- destroyed = true;
16
- inner?.destroy();
17
- inner = void 0;
18
- } };
19
- };
4
+ var mountProcaptchaPuzzle = (0, _prosopo_procaptcha_common.lazyMount)(loadProcaptchaPuzzle);
20
5
  //#endregion
21
6
  exports.loadProcaptchaPuzzle = loadProcaptchaPuzzle;
22
7
  exports.mountProcaptchaPuzzle = mountProcaptchaPuzzle;
@@ -5,7 +5,7 @@ let _prosopo_procaptcha_common = require("@prosopo/procaptcha-common");
5
5
  let _prosopo_types = require("@prosopo/types");
6
6
  let _prosopo_widget_skeleton = require("@prosopo/widget-skeleton");
7
7
  //#region src/components/procaptchaWidget.ts
8
- var PROCAPTCHA_EXECUTE_EVENT = "procaptcha:execute";
8
+ var IMAGE_SWITCH_HIGHLIGHT_AFTER = 2;
9
9
  var mountProcaptchaPuzzleWidget = (container, props) => {
10
10
  const teardown = new _prosopo_procaptcha_common.Teardown();
11
11
  const config = props.config;
@@ -26,7 +26,8 @@ var mountProcaptchaPuzzleWidget = (container, props) => {
26
26
  let checkbox;
27
27
  let puzzle;
28
28
  const updateState = (0, _prosopo_procaptcha_common.buildUpdateState)(store.state, store.update);
29
- const manager = require_Manager.Manager(config, store.state, updateState, callbacks, frictionlessState, () => honeypot?.getValue());
29
+ const remintsSession = void 0 !== frictionlessState?.sessionId && void 0 !== props.onReload;
30
+ const manager = require_Manager.Manager(config, store.state, updateState, callbacks, frictionlessState, () => honeypot?.getValue(), { widgetReloadsOnFailure: remintsSession });
30
31
  const root = (0, _prosopo_procaptcha_common.createElement)("div");
31
32
  container.appendChild(root);
32
33
  if (frictionlessState?.hp) honeypot = (0, _prosopo_procaptcha_common.mountHoneypot)(root, { encodedQuestion: frictionlessState.hp });
@@ -53,10 +54,10 @@ var mountProcaptchaPuzzleWidget = (container, props) => {
53
54
  await replaceChallenge({ showRetry: true });
54
55
  };
55
56
  const replaceChallenge = async (options) => {
56
- if (frictionlessState?.sessionId && props.onReload) {
57
+ if (remintsSession) {
57
58
  puzzlePhase = "submitting";
58
59
  scheduler.schedule();
59
- props.onReload(lastCoords?.x, lastCoords?.y, options);
60
+ props.onReload?.(lastCoords?.x, lastCoords?.y, options);
60
61
  return;
61
62
  }
62
63
  try {
@@ -85,6 +86,18 @@ var mountProcaptchaPuzzleWidget = (container, props) => {
85
86
  scheduler.schedule();
86
87
  replaceChallenge({ refresh: true });
87
88
  };
89
+ const handleSwitchToImage = () => {
90
+ if ("dragging" !== puzzlePhase) return;
91
+ callbacks.onReload?.();
92
+ showRetry = false;
93
+ puzzlePhase = "submitting";
94
+ scheduler.schedule();
95
+ replaceChallenge({
96
+ refresh: true,
97
+ switchToImage: true
98
+ });
99
+ };
100
+ const canSwitchToImage = (challenge) => true === challenge.imageSwitchAvailable && remintsSession;
88
101
  const handleDismiss = () => {
89
102
  puzzlePhase = "checkbox";
90
103
  challengeData = null;
@@ -108,7 +121,12 @@ var mountProcaptchaPuzzleWidget = (container, props) => {
108
121
  placement: config.placement,
109
122
  anchor: props.container,
110
123
  onDismiss: handleDismiss,
111
- onRefresh: handleRefresh
124
+ audioAlternative: (0, _prosopo_procaptcha_common.audioAlternativeOffer)(props, translator.isReady() ? translator.t("WIDGET.AUDIO_ALTERNATIVE") : ""),
125
+ onRefresh: handleRefresh,
126
+ ...canSwitchToImage(challenge) && {
127
+ onSwitchToImage: handleSwitchToImage,
128
+ imageSwitchHighlighted: (props.startReplacementCount ?? 0) >= IMAGE_SWITCH_HIGHLIGHT_AFTER
129
+ }
112
130
  });
113
131
  const runErrorEffect = () => {
114
132
  if (store.state.error === lastError) return;
@@ -158,18 +176,7 @@ var mountProcaptchaPuzzleWidget = (container, props) => {
158
176
  loading = true;
159
177
  showRetry = false;
160
178
  scheduler.schedule();
161
- let x = 0;
162
- let y = 0;
163
- if (!(0, _prosopo_procaptcha_common.isEventTrusted)(event)) {} else if ("touches" in event && event.touches.length > 0) {
164
- const touch = event.touches[0];
165
- if (touch) {
166
- x = touch.clientX;
167
- y = touch.clientY;
168
- }
169
- } else if ("clientX" in event && "clientY" in event) {
170
- x = event.clientX;
171
- y = event.clientY;
172
- }
179
+ const { x, y } = (0, _prosopo_procaptcha_common.trustedClickCoords)(event);
173
180
  lastCoords = {
174
181
  x,
175
182
  y
@@ -212,8 +219,8 @@ var mountProcaptchaPuzzleWidget = (container, props) => {
212
219
  }
213
220
  })();
214
221
  };
215
- if (props.container) teardown.addEventListener(props.container, PROCAPTCHA_EXECUTE_EVENT, handleExecute);
216
- if (isInvisible) teardown.addEventListener(document, PROCAPTCHA_EXECUTE_EVENT, handleExecute);
222
+ if (props.container) teardown.addEventListener(props.container, _prosopo_procaptcha_common.PROCAPTCHA_EXECUTE_EVENT, handleExecute);
223
+ if (isInvisible) teardown.addEventListener(document, _prosopo_procaptcha_common.PROCAPTCHA_EXECUTE_EVENT, handleExecute);
217
224
  if (config.language) if (i18n) {
218
225
  if (i18n.language !== config.language) i18n.changeLanguage(config.language);
219
226
  } else (0, _prosopo_locale.loadI18next)(false, config.language);
@@ -104,6 +104,7 @@ var mountPuzzleCanvas = (initialProps) => {
104
104
  let shakeTimer;
105
105
  let announceTimer;
106
106
  let keyboardDragging = false;
107
+ let hasMovedPiece = false;
107
108
  const baseId = `prosopo-puzzle-${instanceCount++}`;
108
109
  const instructionId = `${baseId}-instruction`;
109
110
  const keyboardHintId = `${baseId}-keyboard-hint`;
@@ -182,6 +183,13 @@ var mountPuzzleCanvas = (initialProps) => {
182
183
  top: "50%",
183
184
  transform: "translateY(-50%)"
184
185
  } });
186
+ const switchSlot = (0, _prosopo_procaptcha_common.createElement)("div", { style: {
187
+ position: "absolute",
188
+ left: "8px",
189
+ top: "50%",
190
+ transform: "translateY(-50%)",
191
+ zIndex: "1"
192
+ } });
185
193
  const instruction = (0, _prosopo_procaptcha_common.createElement)("div", {
186
194
  style: {
187
195
  position: "relative",
@@ -194,7 +202,11 @@ var mountPuzzleCanvas = (initialProps) => {
194
202
  fontWeight: 500,
195
203
  transition: "color 0.3s ease, border-color 0.3s ease"
196
204
  },
197
- children: [instructionText, refreshSlot]
205
+ children: [
206
+ instructionText,
207
+ refreshSlot,
208
+ switchSlot
209
+ ]
198
210
  });
199
211
  const panel = (0, _prosopo_procaptcha_common.createElement)("div", {
200
212
  style: {
@@ -215,6 +227,29 @@ var mountPuzzleCanvas = (initialProps) => {
215
227
  }
216
228
  });
217
229
  const refreshButton = props.onRefresh ? (0, _prosopo_procaptcha_common.mountReloadButton)(refreshSlot, refreshButtonProps()) : void 0;
230
+ const imageSwitchButtonProps = () => ({
231
+ themeColor: _prosopo_widget_skeleton.lightTheme === props.theme ? "light" : "dark",
232
+ label: t("WIDGET.PUZZLE.SWITCH_TO_IMAGE", { defaultValue: "Switch to an image challenge" }),
233
+ tooltipPinned: true === props.imageSwitchHighlighted && !hasMovedPiece,
234
+ onSwitch: () => {
235
+ if (!props.submitting && !dragging) props.onSwitchToImage?.();
236
+ }
237
+ });
238
+ let imageSwitchButton;
239
+ const hidePinnedSwitchTooltip = () => {
240
+ if (hasMovedPiece) return;
241
+ hasMovedPiece = true;
242
+ imageSwitchButton?.update(imageSwitchButtonProps());
243
+ };
244
+ const syncImageSwitchButton = () => {
245
+ if (void 0 === props.onSwitchToImage) {
246
+ imageSwitchButton?.destroy();
247
+ imageSwitchButton = void 0;
248
+ return;
249
+ }
250
+ if (imageSwitchButton) imageSwitchButton.update(imageSwitchButtonProps());
251
+ else imageSwitchButton = (0, _prosopo_procaptcha_common.mountImageSwitchButton)(switchSlot, imageSwitchButtonProps());
252
+ };
218
253
  const surfaceProps = () => ({
219
254
  show: true,
220
255
  placement: props.placement,
@@ -225,6 +260,12 @@ var mountPuzzleCanvas = (initialProps) => {
225
260
  });
226
261
  const surface = (0, _prosopo_procaptcha_common.mountChallengeSurface)(surfaceProps());
227
262
  surface.content.append(style, announcer, keyboardHint, panel);
263
+ const audioAlternativeFooterProps = () => ({
264
+ offer: props.audioAlternative,
265
+ theme: props.theme,
266
+ width: CONTAINER_WIDTH
267
+ });
268
+ const audioAlternativeFooter = (0, _prosopo_procaptcha_common.mountAudioAlternativeFooter)(panel, audioAlternativeFooterProps());
228
269
  const announce = (message, delayMs = 0) => {
229
270
  if (void 0 !== announceTimer) {
230
271
  clearTimeout(announceTimer);
@@ -293,11 +334,15 @@ var mountPuzzleCanvas = (initialProps) => {
293
334
  });
294
335
  (0, _prosopo_procaptcha_common.applyStyles)(refreshSlot, { visibility: props.submitting ? "hidden" : "visible" });
295
336
  refreshButton?.update(refreshButtonProps());
337
+ syncImageSwitchButton();
338
+ (0, _prosopo_procaptcha_common.applyStyles)(switchSlot, { visibility: props.submitting ? "hidden" : "visible" });
296
339
  (0, _prosopo_procaptcha_common.applyStyles)(area, {
297
340
  background: `linear-gradient(135deg, ${theme.palette.surface} 0%, ${theme.palette.primaryContainer.main} 50%, ${theme.palette.surface} 100%)`,
298
341
  opacity: props.submitting ? .6 : 1,
299
- pointerEvents: props.submitting ? "none" : "auto"
342
+ pointerEvents: props.submitting ? "none" : "auto",
343
+ borderRadius: props.audioAlternative ? "0" : "0 0 20px 20px"
300
344
  });
345
+ audioAlternativeFooter.update(audioAlternativeFooterProps());
301
346
  for (const tile of backgroundTiles) tile.src = props.background;
302
347
  (0, _prosopo_procaptcha_common.applyAttributes)(piece, {
303
348
  tabindex: props.submitting ? -1 : 0,
@@ -346,6 +391,7 @@ var mountPuzzleCanvas = (initialProps) => {
346
391
  dragging = true;
347
392
  keyboardDragging = false;
348
393
  puzzleEvents = [];
394
+ hidePinnedSwitchTooltip();
349
395
  const offset = containerOffset();
350
396
  dragOffset = {
351
397
  x: clientX - offset.x - posX,
@@ -358,6 +404,7 @@ var mountPuzzleCanvas = (initialProps) => {
358
404
  if (!keyboardDragging) {
359
405
  keyboardDragging = true;
360
406
  puzzleEvents = [];
407
+ hidePinnedSwitchTooltip();
361
408
  }
362
409
  posX = clamp(posX + deltaX, 0, CONTAINER_WIDTH);
363
410
  posY = clamp(posY + deltaY, 0, CONTAINER_HEIGHT);
@@ -439,7 +486,9 @@ var mountPuzzleCanvas = (initialProps) => {
439
486
  announce(t("WIDGET.PUZZLE.CHECKING", { defaultValue: "Checking your answer" }));
440
487
  };
441
488
  const announceRetry = () => {
442
- announce(t("WIDGET.PUZZLE.RETRY_ANNOUNCEMENT", { defaultValue: "Not quite. A new puzzle has loaded and the piece is back at the start." }));
489
+ const retry = t("WIDGET.PUZZLE.RETRY_ANNOUNCEMENT", { defaultValue: "Not quite. A new puzzle has loaded and the piece is back at the start." });
490
+ const offerSwitch = void 0 !== props.onSwitchToImage && true === props.imageSwitchHighlighted;
491
+ announce(offerSwitch ? `${retry} ${t("WIDGET.PUZZLE.SWITCH_TO_IMAGE_ANNOUNCEMENT", { defaultValue: "You can also switch to an image challenge." })}` : retry);
443
492
  };
444
493
  if (props.submitting) announceSubmitting();
445
494
  else if (props.showRetry) announceRetry();
@@ -463,7 +512,9 @@ var mountPuzzleCanvas = (initialProps) => {
463
512
  },
464
513
  destroy: () => {
465
514
  teardown.run();
515
+ audioAlternativeFooter.destroy();
466
516
  refreshButton?.destroy();
517
+ imageSwitchButton?.destroy();
467
518
  surface.destroy();
468
519
  }
469
520
  };
@@ -6,7 +6,7 @@ let _prosopo_common = require("@prosopo/common");
6
6
  let _prosopo_util = require("@prosopo/util");
7
7
  let _prosopo_util_crypto = require("@prosopo/util-crypto");
8
8
  //#region src/services/Manager.ts
9
- var Manager = (configInput, state, onStateUpdate, callbacks, frictionlessState, getHoneypotValue) => {
9
+ var Manager = (configInput, state, onStateUpdate, callbacks, frictionlessState, getHoneypotValue, options = {}) => {
10
10
  const events = (0, _prosopo_procaptcha_common.getDefaultEvents)(callbacks);
11
11
  let storedChallengeResponse;
12
12
  let storedProviderApi;
@@ -15,7 +15,7 @@ var Manager = (configInput, state, onStateUpdate, callbacks, frictionlessState,
15
15
  let storedClickX;
16
16
  let storedClickY;
17
17
  let previousProviderUrl;
18
- let spentSessionId;
18
+ const spentSession = (0, _prosopo_procaptcha_common.createSpentSessionGuard)();
19
19
  const defaultState = () => {
20
20
  return {
21
21
  showModal: false,
@@ -28,21 +28,13 @@ var Manager = (configInput, state, onStateUpdate, callbacks, frictionlessState,
28
28
  account: void 0
29
29
  };
30
30
  };
31
- const clearTimeout = () => {
32
- window.clearTimeout(Number(state.timeout));
33
- updateState({ timeout: void 0 });
34
- };
35
31
  const onFailed = () => {
36
32
  updateState({
37
33
  isHuman: false,
38
34
  loading: false
39
35
  });
40
36
  events.onFailed();
41
- resetState(frictionlessState?.restart);
42
- };
43
- const clearSuccessfulChallengeTimeout = () => {
44
- window.clearTimeout(Number(state.successfullChallengeTimeout));
45
- updateState({ successfullChallengeTimeout: void 0 });
37
+ resetState(options.widgetReloadsOnFailure ? void 0 : frictionlessState?.restart);
46
38
  };
47
39
  const getConfig = () => {
48
40
  const config = {
@@ -61,9 +53,9 @@ var Manager = (configInput, state, onStateUpdate, callbacks, frictionlessState,
61
53
  return state.dappAccount;
62
54
  };
63
55
  const updateState = (0, _prosopo_procaptcha_common.buildUpdateState)(state, onStateUpdate);
56
+ const lifecycle = (0, _prosopo_procaptcha_common.createManagerLifecycle)(state, updateState);
64
57
  const resetState = (frictionlessRestart) => {
65
- clearTimeout();
66
- clearSuccessfulChallengeTimeout();
58
+ lifecycle.clearTimers();
67
59
  updateState(defaultState());
68
60
  events.onReset();
69
61
  if (frictionlessRestart) frictionlessRestart();
@@ -75,14 +67,11 @@ var Manager = (configInput, state, onStateUpdate, callbacks, frictionlessState,
75
67
  storedClickY = void 0;
76
68
  };
77
69
  const setValidChallengeTimeout = () => {
78
- const timeMillis = getConfig().captchas.puzzle.solutionTimeout;
79
- const successfullChallengeTimeout = setTimeout(() => {
80
- if (disposed) return;
70
+ lifecycle.expireSolutionAfter(getConfig().captchas.puzzle.solutionTimeout, () => {
81
71
  updateState({ isHuman: false });
82
72
  events.onExpired();
83
73
  resetState(frictionlessState?.restart);
84
- }, timeMillis);
85
- updateState({ successfullChallengeTimeout });
74
+ });
86
75
  };
87
76
  const start = async (x = 0, y = 0) => {
88
77
  await (0, _prosopo_procaptcha_common.providerRetry)(async () => {
@@ -117,7 +106,7 @@ var Manager = (configInput, state, onStateUpdate, callbacks, frictionlessState,
117
106
  previousProviderUrl = providerUrl;
118
107
  const providerApi = new _prosopo_api.ProviderApi(providerUrl, getDappAccount());
119
108
  const challengeSessionId = frictionlessState?.sessionId;
120
- if (challengeSessionId && challengeSessionId === spentSessionId) {
109
+ if (spentSession.isSpent(challengeSessionId)) {
121
110
  updateState({
122
111
  loading: false,
123
112
  error: {
@@ -130,7 +119,7 @@ var Manager = (configInput, state, onStateUpdate, callbacks, frictionlessState,
130
119
  }
131
120
  const simdReadingsOnChallenge = frictionlessState?.getSimdReadings ? await frictionlessState.getSimdReadings(0) : void 0;
132
121
  const challenge = await providerApi.getPuzzleCaptchaChallenge(userAccount, getDappAccount(), challengeSessionId, simdReadingsOnChallenge);
133
- if (challengeSessionId) spentSessionId = challengeSessionId;
122
+ spentSession.markSpent(challengeSessionId);
134
123
  if (challenge.error) {
135
124
  updateState({
136
125
  loading: false,
@@ -169,18 +158,7 @@ var Manager = (configInput, state, onStateUpdate, callbacks, frictionlessState,
169
158
  data: (0, _polkadot_util_string.stringToHex)(challenge[_prosopo_types.ApiParams.timestamp].toString()),
170
159
  type: "bytes"
171
160
  });
172
- let encryptedBehavioralData;
173
- if (frictionlessState?.encryptBehavioralData && (frictionlessState?.behaviorCollector1 || frictionlessState?.behaviorCollector2 || frictionlessState?.behaviorCollector3 || frictionlessState?.behaviorCollector4)) try {
174
- const behavioralData = {
175
- collector1: frictionlessState.behaviorCollector1?.getData() || [],
176
- collector2: frictionlessState.behaviorCollector2?.getData() || [],
177
- collector3: frictionlessState.behaviorCollector3?.getData() || [],
178
- collector4: frictionlessState.behaviorCollector4?.getData() || [],
179
- deviceCapability: frictionlessState.deviceCapability || "unknown"
180
- };
181
- const dataToEncrypt = frictionlessState.packBehavioralData ? frictionlessState.packBehavioralData(behavioralData) : behavioralData;
182
- encryptedBehavioralData = await frictionlessState.encryptBehavioralData(JSON.stringify(dataToEncrypt));
183
- } catch {}
161
+ const encryptedBehavioralData = await (0, _prosopo_procaptcha_common.encryptBehavioralDataForSubmit)(frictionlessState);
184
162
  let salt;
185
163
  if (storedClickX !== void 0 && storedClickY !== void 0) {
186
164
  const coords = [storedClickX, storedClickY];
@@ -190,7 +168,7 @@ var Manager = (configInput, state, onStateUpdate, callbacks, frictionlessState,
190
168
  const simdReadings = await (0, _prosopo_procaptcha_common.getSimdReadingsForSubmit)(frictionlessState);
191
169
  const clientMetaData = (0, _prosopo_procaptcha_common.buildClientMetaData)(getHoneypotValue?.(), getConfig().clientSessionId);
192
170
  const verifiedSolution = await providerApi.submitPuzzleCaptchaSolution(challenge, getAccount().account.account.address, getDappAccount(), finalX, finalY, puzzleEvents, userTimestampSignature.signature.toString(), encryptedBehavioralData, salt, simdReadings, clientMetaData);
193
- if (disposed) return false;
171
+ if (lifecycle.isDisposed()) return false;
194
172
  if (verifiedSolution[_prosopo_types.ApiParams.verified]) {
195
173
  updateState({
196
174
  isHuman: true,
@@ -218,23 +196,11 @@ var Manager = (configInput, state, onStateUpdate, callbacks, frictionlessState,
218
196
  throw error;
219
197
  }
220
198
  };
221
- let disposed = false;
222
- /**
223
- * Stops this manager's challenge and solution-expiry timers without firing
224
- * any event. Left running after the widget is destroyed (reset(), a
225
- * restart, an SPA route change) they fired onExpired/onReset later on,
226
- * which also cleared the replacement widget's token from the form.
227
- */
228
- const dispose = () => {
229
- disposed = true;
230
- window.clearTimeout(Number(state.timeout));
231
- window.clearTimeout(Number(state.successfullChallengeTimeout));
232
- };
233
199
  return {
234
200
  start,
235
201
  submitSolution,
236
202
  resetState,
237
- dispose
203
+ dispose: lifecycle.dispose
238
204
  };
239
205
  };
240
206
  //#endregion
@@ -1,7 +1,7 @@
1
- import type { ProcaptchaProps } from "@prosopo/types";
1
+ import { type WidgetMountFn } from "@prosopo/procaptcha-common";
2
2
  import type { ProcaptchaPuzzleHandle } from "./procaptchaWidget.js";
3
3
  export type { ProcaptchaPuzzleHandle };
4
- export type ProcaptchaPuzzleMountFn = (container: HTMLElement, props: ProcaptchaProps) => ProcaptchaPuzzleHandle;
4
+ export type ProcaptchaPuzzleMountFn = WidgetMountFn<ProcaptchaPuzzleHandle>;
5
5
  export declare const loadProcaptchaPuzzle: () => Promise<ProcaptchaPuzzleMountFn>;
6
- export declare const mountProcaptchaPuzzle: (container: HTMLElement, props: ProcaptchaProps) => ProcaptchaPuzzleHandle;
6
+ export declare const mountProcaptchaPuzzle: ProcaptchaPuzzleMountFn;
7
7
  //# sourceMappingURL=procaptchaPuzzle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"procaptchaPuzzle.d.ts","sourceRoot":"","sources":["../../src/components/procaptchaPuzzle.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAEpE,YAAY,EAAE,sBAAsB,EAAE,CAAC;AAEvC,MAAM,MAAM,uBAAuB,GAAG,CACrC,SAAS,EAAE,WAAW,EACtB,KAAK,EAAE,eAAe,KAClB,sBAAsB,CAAC;AAM5B,eAAO,MAAM,oBAAoB,QACtB,OAAO,CAAC,uBAAuB,CAC2B,CAAC;AAEtE,eAAO,MAAM,qBAAqB,cACtB,WAAW,SACf,eAAe,KACpB,sBAkBF,CAAC"}
1
+ {"version":3,"file":"procaptchaPuzzle.d.ts","sourceRoot":"","sources":["../../src/components/procaptchaPuzzle.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,aAAa,EAAa,MAAM,4BAA4B,CAAC;AAC3E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAEpE,YAAY,EAAE,sBAAsB,EAAE,CAAC;AAEvC,MAAM,MAAM,uBAAuB,GAAG,aAAa,CAAC,sBAAsB,CAAC,CAAC;AAE5E,eAAO,MAAM,oBAAoB,QACtB,OAAO,CAAC,uBAAuB,CAC2B,CAAC;AAEtE,eAAO,MAAM,qBAAqB,EAAE,uBACJ,CAAC"}
@@ -1,21 +1,6 @@
1
+ import { lazyMount } from "@prosopo/procaptcha-common";
1
2
  //#region src/components/procaptchaPuzzle.ts
2
- /**
3
- * Dynamic import so the puzzle widget lands in its own chunk, replacing the
4
- * `lazy()` + `<Suspense>` pair that used to provide the split.
5
- */
6
3
  var loadProcaptchaPuzzle = async () => (await import("./procaptchaWidget.js")).mountProcaptchaPuzzleWidget;
7
- var mountProcaptchaPuzzle = (container, props) => {
8
- let destroyed = false;
9
- let inner;
10
- loadProcaptchaPuzzle().then((mount) => {
11
- if (destroyed) return;
12
- inner = mount(container, props);
13
- });
14
- return { destroy: () => {
15
- destroyed = true;
16
- inner?.destroy();
17
- inner = void 0;
18
- } };
19
- };
4
+ var mountProcaptchaPuzzle = lazyMount(loadProcaptchaPuzzle);
20
5
  //#endregion
21
6
  export { loadProcaptchaPuzzle, mountProcaptchaPuzzle };
@@ -1 +1 @@
1
- {"version":3,"file":"procaptchaPuzzle.js","sourceRoot":"","sources":["../../src/components/procaptchaPuzzle.ts"],"names":[],"mappings":"AA4BA,MAAM,CAAC,MAAM,oBAAoB,GAChC,KAAK,IAAsC,EAAE,CAC5C,CAAC,MAAM,MAAM,CAAC,uBAAuB,CAAC,CAAC,CAAC,2BAA2B,CAAC;AAEtE,MAAM,CAAC,MAAM,qBAAqB,GAAG,CACpC,SAAsB,EACtB,KAAsB,EACG,EAAE;IAC3B,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,IAAI,KAAyC,CAAC;IAE9C,KAAK,oBAAoB,EAAE,CAAC,IAAI,CAAC,CAAC,KAA8B,EAAE,EAAE;QACnE,IAAI,SAAS,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QACD,KAAK,GAAG,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,OAAO;QACN,OAAO,EAAE,GAAG,EAAE;YACb,SAAS,GAAG,IAAI,CAAC;YACjB,KAAK,EAAE,OAAO,EAAE,CAAC;YACjB,KAAK,GAAG,SAAS,CAAC;QACnB,CAAC;KACD,CAAC;AACH,CAAC,CAAC"}
1
+ {"version":3,"file":"procaptchaPuzzle.js","sourceRoot":"","sources":["../../src/components/procaptchaPuzzle.ts"],"names":[],"mappings":"AAcA,OAAO,EAAsB,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAO3E,MAAM,CAAC,MAAM,oBAAoB,GAChC,KAAK,IAAsC,EAAE,CAC5C,CAAC,MAAM,MAAM,CAAC,uBAAuB,CAAC,CAAC,CAAC,2BAA2B,CAAC;AAEtE,MAAM,CAAC,MAAM,qBAAqB,GACjC,SAAS,CAAC,oBAAoB,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"procaptchaWidget.d.ts","sourceRoot":"","sources":["../../src/components/procaptchaWidget.ts"],"names":[],"mappings":"AAiCA,OAAO,EAGN,KAAK,eAAe,EAIpB,MAAM,gBAAgB,CAAC;AAUxB,MAAM,WAAW,sBAAsB;IACtC,OAAO,IAAI,IAAI,CAAC;CAChB;AAED,eAAO,MAAM,2BAA2B,cAC5B,WAAW,SACf,eAAe,KACpB,sBAqYF,CAAC;AAEF,eAAe,2BAA2B,CAAC"}
1
+ {"version":3,"file":"procaptchaWidget.d.ts","sourceRoot":"","sources":["../../src/components/procaptchaWidget.ts"],"names":[],"mappings":"AAmCA,OAAO,EAGN,KAAK,eAAe,EAIpB,MAAM,gBAAgB,CAAC;AAWxB,MAAM,WAAW,sBAAsB;IACtC,OAAO,IAAI,IAAI,CAAC;CAChB;AAED,eAAO,MAAM,2BAA2B,cAC5B,WAAW,SACf,eAAe,KACpB,sBAkZF,CAAC;AAEF,eAAe,2BAA2B,CAAC"}