@prosopo/procaptcha-react 2.12.0 → 2.13.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 (31) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +8 -8
  2. package/.turbo/turbo-build$colon$tsc.log +22 -22
  3. package/.turbo/turbo-build.log +9 -9
  4. package/CHANGELOG.md +86 -0
  5. package/dist/cjs/components/button.cjs +14 -14
  6. package/dist/cjs/components/captchaComponent.cjs +42 -27
  7. package/dist/cjs/components/captchaWidget.cjs +41 -24
  8. package/dist/cjs/components/modal.cjs +7 -3
  9. package/dist/components/button.d.ts.map +1 -1
  10. package/dist/components/button.js +16 -16
  11. package/dist/components/button.js.map +1 -1
  12. package/dist/components/captchaComponent.d.ts.map +1 -1
  13. package/dist/components/captchaComponent.js +44 -29
  14. package/dist/components/captchaComponent.js.map +1 -1
  15. package/dist/components/captchaWidget.d.ts.map +1 -1
  16. package/dist/components/captchaWidget.js +43 -26
  17. package/dist/components/captchaWidget.js.map +1 -1
  18. package/dist/components/modal.d.ts.map +1 -1
  19. package/dist/components/modal.js +8 -4
  20. package/dist/components/modal.js.map +1 -1
  21. package/dist/tests/button.unit.test.js +42 -3
  22. package/dist/tests/button.unit.test.js.map +1 -1
  23. package/dist/tests/captchaComponent.unit.test.js +4 -4
  24. package/dist/tests/captchaComponent.unit.test.js.map +1 -1
  25. package/dist/tests/captchaWidget.unit.test.js +91 -11
  26. package/dist/tests/captchaWidget.unit.test.js.map +1 -1
  27. package/dist/tests/modal.unit.test.js +26 -10
  28. package/dist/tests/modal.unit.test.js.map +1 -1
  29. package/dist/tests/procaptchaWidget.unit.test.js +10 -8
  30. package/dist/tests/procaptchaWidget.unit.test.js.map +1 -1
  31. package/package.json +8 -8
@@ -1,5 +1,5 @@
1
1
 
2
- > @prosopo/procaptcha-react@2.12.0 build:cjs
2
+ > @prosopo/procaptcha-react@2.13.1 build:cjs
3
3
  > NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV
4
4
 
5
5
  ViteCommonJSConfig: .
@@ -7,8 +7,8 @@ ViteCommonJSConfig: .
7
7
  tsConfigPaths: [
8
8
  '/home/runner/work/captcha/captcha/packages/common/tsconfig.json',
9
9
  '/home/runner/work/captcha/captcha/packages/locale/tsconfig.json',
10
- '/home/runner/work/captcha/captcha/packages/logger/tsconfig.json',
11
10
  '/home/runner/work/captcha/captcha/packages/util/tsconfig.json',
11
+ '/home/runner/work/captcha/captcha/packages/logger/tsconfig.json',
12
12
  '/home/runner/work/captcha/captcha/packages/types/tsconfig.json',
13
13
  '/home/runner/work/captcha/captcha/packages/util-crypto/tsconfig.json',
14
14
  '/home/runner/work/captcha/captcha/packages/procaptcha/tsconfig.json',
@@ -27,8 +27,8 @@ ViteCommonJSConfig: .
27
27
  externals: [
28
28
  '@prosopo/common',
29
29
  '@prosopo/locale',
30
- '@prosopo/logger',
31
30
  '@prosopo/util',
31
+ '@prosopo/logger',
32
32
  '@prosopo/types',
33
33
  '@prosopo/util-crypto',
34
34
  '@prosopo/procaptcha',
@@ -54,11 +54,11 @@ dist/cjs/util/index.cjs 0.56 kB │ gzip: 0.35 kB
54
54
  dist/cjs/components/procaptcha.cjs 0.75 kB │ gzip: 0.41 kB
55
55
  dist/cjs/components/index.cjs 0.89 kB │ gzip: 0.22 kB
56
56
  dist/cjs/index.cjs 1.04 kB │ gzip: 0.28 kB
57
- dist/cjs/components/modal.cjs 1.10 kB │ gzip: 0.55 kB
57
+ dist/cjs/components/modal.cjs 1.39 kB │ gzip: 0.64 kB
58
58
  dist/cjs/components/collector.cjs 1.53 kB │ gzip: 0.68 kB
59
- dist/cjs/components/button.cjs 3.31 kB │ gzip: 1.23 kB
59
+ dist/cjs/components/button.cjs 3.28 kB │ gzip: 1.24 kB
60
60
  dist/cjs/components/procaptchaWidget.cjs 6.33 kB │ gzip: 2.06 kB
61
- dist/cjs/components/captchaWidget.cjs 6.47 kB │ gzip: 2.29 kB
62
- dist/cjs/components/captchaComponent.cjs 7.25 kB │ gzip: 1.89 kB
61
+ dist/cjs/components/captchaWidget.cjs 7.49 kB │ gzip: 2.70 kB
62
+ dist/cjs/components/captchaComponent.cjs 8.29 kB │ gzip: 2.17 kB
63
63
 
64
- ✓ built in 48ms
64
+ ✓ built in 40ms
@@ -1,11 +1,11 @@
1
1
 
2
- > @prosopo/procaptcha-react@2.12.0 build:tsc
2
+ > @prosopo/procaptcha-react@2.13.1 build:tsc
3
3
  > tsc --build --verbose
4
4
 
5
- 11:04:34 AM - Projects in this build:
5
+ 10:40:51 PM - Projects in this build:
6
6
  * ../../dev/config/tsconfig.json
7
- * ../locale/tsconfig.json
8
7
  * ../util/tsconfig.json
8
+ * ../locale/tsconfig.json
9
9
  * ../logger/tsconfig.json
10
10
  * ../util-crypto/tsconfig.json
11
11
  * ../types/tsconfig.json
@@ -22,41 +22,41 @@
22
22
  * ../procaptcha/tsconfig.json
23
23
  * tsconfig.json
24
24
 
25
- 11:04:34 AM - Project '../../dev/config/tsconfig.json' is up to date because newest input '../../dev/config/src/vite/NodejsPolarsDirnamePlugin.ts' is older than output '../../dev/config/tsconfig.tsbuildinfo'
25
+ 10:40:51 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'
26
26
 
27
- 11:04:34 AM - Project '../locale/tsconfig.json' is up to date because newest input '../locale/src/process.ts' is older than output '../locale/tsconfig.tsbuildinfo'
27
+ 10:40:51 PM - Project '../util/tsconfig.json' is up to date because newest input '../util/src/url.ts' is older than output '../util/tsconfig.tsbuildinfo'
28
28
 
29
- 11:04:34 AM - Project '../util/tsconfig.json' is up to date because newest input '../util/src/ofLen.ts' is older than output '../util/tsconfig.tsbuildinfo'
29
+ 10:40:51 PM - Project '../locale/tsconfig.json' is up to date because newest input '../locale/src/process.ts' is older than output '../locale/tsconfig.tsbuildinfo'
30
30
 
31
- 11:04:34 AM - Project '../logger/tsconfig.json' is up to date because newest input '../logger/src/index.ts' is older than output '../logger/tsconfig.tsbuildinfo'
31
+ 10:40:51 PM - Project '../logger/tsconfig.json' is up to date because newest input '../logger/src/tests/loggerBrowser.unit.test.ts' is older than output '../logger/tsconfig.tsbuildinfo'
32
32
 
33
- 11:04:34 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'
33
+ 10:40:51 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'
34
34
 
35
- 11:04:34 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'
35
+ 10:40:51 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'
36
36
 
37
- 11:04:34 AM - Project '../common/tsconfig.json' is up to date because newest input '../common/src/utils/batches.ts' is older than output '../common/tsconfig.tsbuildinfo'
37
+ 10:40:51 PM - Project '../common/tsconfig.json' is up to date because newest input '../common/src/tests/utils/objectUtils.unit.test.ts' is older than output '../common/tsconfig.tsbuildinfo'
38
38
 
39
- 11:04:34 AM - Project '../datasets/tsconfig.json' is up to date because newest input '../datasets/src/index.ts' is older than output '../datasets/tsconfig.tsbuildinfo'
39
+ 10:40:51 PM - Project '../datasets/tsconfig.json' is up to date because newest input '../datasets/src/tests/captcha.unit.test.ts' is older than output '../datasets/tsconfig.tsbuildinfo'
40
40
 
41
- 11:04:34 AM - Project '../api/tsconfig.json' is up to date because newest input '../api/src/index.ts' is older than output '../api/tsconfig.tsbuildinfo'
41
+ 10:40:51 PM - Project '../api/tsconfig.json' is up to date because newest input '../api/src/index.ts' is older than output '../api/tsconfig.tsbuildinfo'
42
42
 
43
- 11:04:34 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'
43
+ 10:40:51 PM - 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'
44
44
 
45
- 11:04:34 AM - Project '../widget-skeleton/tsconfig.json' is up to date because newest input '../widget-skeleton/src/index.ts' is older than output '../widget-skeleton/tsconfig.tsbuildinfo'
45
+ 10:40:51 PM - 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'
46
46
 
47
- 11:04:34 AM - Project '../fingerprintjs/tsconfig.json' is up to date because newest input '../fingerprintjs/src/utils/async.ts' is older than output '../fingerprintjs/tsconfig.tsbuildinfo'
47
+ 10:40:51 PM - Project '../fingerprintjs/tsconfig.json' is up to date because newest input '../fingerprintjs/src/utils/browser.ts' is older than output '../fingerprintjs/tsconfig.tsbuildinfo'
48
48
 
49
- 11:04:34 AM - Project '../fingerprint/tsconfig.json' is up to date because newest input '../fingerprint/src/index.ts' is older than output '../fingerprint/tsconfig.tsbuildinfo'
49
+ 10:40:51 PM - Project '../fingerprint/tsconfig.json' is up to date because newest input '../fingerprint/src/index.ts' is older than output '../fingerprint/tsconfig.tsbuildinfo'
50
50
 
51
- 11:04:34 AM - Project '../keyring/tsconfig.json' is up to date because newest input '../keyring/src/keyring/keyring.test.ts' is older than output '../keyring/tsconfig.tsbuildinfo'
51
+ 10:40:51 PM - Project '../keyring/tsconfig.json' is up to date because newest input '../keyring/src/index.ts' is older than output '../keyring/tsconfig.tsbuildinfo'
52
52
 
53
- 11:04:34 AM - Project '../account/tsconfig.json' is up to date because newest input '../account/src/custom.d.ts' is older than output '../account/tsconfig.tsbuildinfo'
53
+ 10:40:51 PM - Project '../account/tsconfig.json' is up to date because newest input '../account/src/workers/cryptoWorker.ts' is older than output '../account/tsconfig.tsbuildinfo'
54
54
 
55
- 11:04:34 AM - Project '../procaptcha-common/tsconfig.json' is up to date because newest input '../procaptcha-common/src/tests/challengeSurfaceRender.unit.test.ts' is older than output '../procaptcha-common/tsconfig.tsbuildinfo'
55
+ 10:40:51 PM - Project '../procaptcha-common/tsconfig.json' is up to date because newest input '../procaptcha-common/src/tests/testModeBanner.unit.test.ts' is older than output '../procaptcha-common/tsconfig.tsbuildinfo'
56
56
 
57
- 11:04:34 AM - Project '../procaptcha/tsconfig.json' is up to date because newest input '../procaptcha/src/modules/Manager.ts' is older than output '../procaptcha/tsconfig.tsbuildinfo'
57
+ 10:40:51 PM - Project '../procaptcha/tsconfig.json' is up to date because newest input '../procaptcha/src/modules/Manager.ts' is older than output '../procaptcha/tsconfig.tsbuildinfo'
58
58
 
59
- 11:04:34 AM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
59
+ 10:40:51 PM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
60
60
 
61
- 11:04:34 AM - Building project '/home/runner/work/captcha/captcha/packages/procaptcha-react/tsconfig.json'...
61
+ 10:40:51 PM - Building project '/home/runner/work/captcha/captcha/packages/procaptcha-react/tsconfig.json'...
62
62
 
@@ -1,9 +1,9 @@
1
1
 
2
- > @prosopo/procaptcha-react@2.12.0 build
2
+ > @prosopo/procaptcha-react@2.13.1 build
3
3
  > npm run build:cross-env -- --mode ${NODE_ENV:-development}
4
4
 
5
5
 
6
- > @prosopo/procaptcha-react@2.12.0 build:cross-env
6
+ > @prosopo/procaptcha-react@2.13.1 build:cross-env
7
7
  > vite build --config vite.esm.config.ts --mode production
8
8
 
9
9
  ViteEsmConfig: .
@@ -11,8 +11,8 @@ ViteEsmConfig: .
11
11
  tsConfigPaths: [
12
12
  '/home/runner/work/captcha/captcha/packages/common/tsconfig.json',
13
13
  '/home/runner/work/captcha/captcha/packages/locale/tsconfig.json',
14
- '/home/runner/work/captcha/captcha/packages/logger/tsconfig.json',
15
14
  '/home/runner/work/captcha/captcha/packages/util/tsconfig.json',
15
+ '/home/runner/work/captcha/captcha/packages/logger/tsconfig.json',
16
16
  '/home/runner/work/captcha/captcha/packages/types/tsconfig.json',
17
17
  '/home/runner/work/captcha/captcha/packages/util-crypto/tsconfig.json',
18
18
  '/home/runner/work/captcha/captcha/packages/procaptcha/tsconfig.json',
@@ -31,8 +31,8 @@ ViteEsmConfig: .
31
31
  externals: [
32
32
  '@prosopo/common',
33
33
  '@prosopo/locale',
34
- '@prosopo/logger',
35
34
  '@prosopo/util',
35
+ '@prosopo/logger',
36
36
  '@prosopo/types',
37
37
  '@prosopo/util-crypto',
38
38
  '@prosopo/procaptcha',
@@ -59,11 +59,11 @@ dist/components/index.js 0.53 kB │ gzip: 0.19 kB
59
59
  dist/util/index.js 0.56 kB │ gzip: 0.35 kB
60
60
  dist/index.js 0.65 kB │ gzip: 0.21 kB
61
61
  dist/components/procaptcha.js 0.68 kB │ gzip: 0.38 kB
62
- dist/components/modal.js 1.01 kB │ gzip: 0.53 kB
62
+ dist/components/modal.js 1.22 kB │ gzip: 0.61 kB
63
63
  dist/components/collector.js 1.44 kB │ gzip: 0.66 kB
64
- dist/components/button.js 3.07 kB │ gzip: 1.21 kB
65
- dist/components/captchaWidget.js 5.98 kB │ gzip: 2.27 kB
64
+ dist/components/button.js 3.03 kB │ gzip: 1.22 kB
66
65
  dist/components/procaptchaWidget.js 6.00 kB │ gzip: 2.05 kB
67
- dist/components/captchaComponent.js 6.34 kB │ gzip: 1.85 kB
66
+ dist/components/captchaWidget.js 6.92 kB │ gzip: 2.68 kB
67
+ dist/components/captchaComponent.js 7.28 kB │ gzip: 2.13 kB
68
68
 
69
- ✓ built in 45ms
69
+ ✓ built in 40ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,91 @@
1
1
  # @prosopo/procaptcha-react
2
2
 
3
+ ## 2.13.1
4
+ ### Patch Changes
5
+
6
+ - aed164d: Make asset signing survive contact with a CDN that actually enforces it.
7
+
8
+ Signing image URLs is optional and has been off in practice, so two paths that only break when a zone starts checking tokens have never been exercised. Both are fixed here, so enabling enforcement is a config change rather than a config change plus an incident.
9
+
10
+ **A dataset could not be imported.** Hashing an item downloads the image from the URL stored in the dataset, and that URL is the canonical, unsigned one. Against an enforcing zone every one of those downloads is rejected, so a dataset becomes impossible to import on exactly the configuration it is meant to be served from. `downloadImage` now signs the URL when a signing key is configured, and leaves it alone when one is not.
11
+
12
+ **A failed image never recovered.** The widget retried a broken image by appending a cache-busting query parameter. A token is a signature over the query string, so on a signed URL that parameter invalidates it and every retry is rejected — a transient failure turned permanent. The retry now re-requests the signed URL unchanged, and keeps the cache-buster for unsigned URLs, where it is still worth having and costs nothing.
13
+
14
+ The token scheme itself now lives in one place, `@prosopo/datasets`, rather than being written out twice. The provider keeps its synchronous implementation, because `resolveAsset` cannot await, but both ends build the string to hash and encode the result through the same helpers.
15
+ - 3d45c37: Stop the widget's hover highlights costing a phone user their first tap.
16
+
17
+ Tapping the checkbox on an iPhone did nothing the first time. The second tap worked. The cause is a rule iOS applies to every page: if the first tap on a control changes what is under the finger, Safari treats it as "show me the hover state" rather than "activate this", and withholds the click. Every control in the widget that lights up on hover was therefore asking to be tapped twice.
18
+
19
+ Hover feedback is now drawn only where a pointer can actually rest on something. On a desktop nothing changes. On a touch screen the highlight never appears and the first tap activates the control, which is what a visitor expects.
20
+
21
+ Four places had it, and only one of them was the checkbox:
22
+
23
+ - the **checkbox** — a state layer around the box
24
+ - the **reload button** in the challenge dialog — a fill change
25
+ - the dialog's **action buttons**, Cancel, Next and Submit — a state layer
26
+ - the **widget container** itself, via a CSS `:hover` rule. This one is easy to miss and matters most: the box that lights up is the one the checkbox sits inside, so it repaints under the finger even when the tap never touches a control with its own hover.
27
+
28
+ The first three ask `matchMedia("(hover: hover)")` before listening for the pointer at all; the fourth is wrapped in the matching media query. The image tiles never had hover feedback and are unchanged.
29
+
30
+ None of this came from the recent randomisation work — the React components these replaced carried the same handlers, so it has been there as long as the widget has.
31
+
32
+ Covered by a test per control asserting no state layer is drawn when the device reports it cannot hover, plus tests for the check itself, including that it assumes a pointer when the environment cannot answer — which is what keeps the existing desktop behaviour, and every existing hover test, intact.
33
+ - 4cc28db: Fix the image challenge laying out two columns instead of three on a phone, which pushed the last images below the screen where they could not be reached.
34
+
35
+ On a Samsung A52s the image grid is 345.578px wide. Each tile asked for `calc(33.333% - 5.33px)` at an 8px gap, so three tiles and the two gaps between them came to 345.584px — **six thousandths of a pixel too wide**. That is enough for the browser to wrap, so the challenge drew two columns and five rows rather than three and three. The panel went from about 370px tall to about 1080px, and three of the nine images ended up below a 718px viewport.
36
+
37
+ The tile width is now written as `calc((100% - 16px) / 3)`, leaving both the subtraction and the division to the browser. There is nothing left to round, so three tiles and their gaps come to exactly the width of the row at every gap the grid draws.
38
+
39
+ This arrived with the dialog randomisation, which replaced a fixed `calc(33.333% - 10px)` with a width derived from the randomised gap. The old value subtracted a whole gap per tile where only two thirds was needed, so it happened to leave about 10px of slack and always fitted. Deriving the width exactly removed the slack, and the rounding then tipped it over. Which gaps break depends on the arithmetic — 8, 11 and 14 round down and overflow, the rest do not — so it looked intermittent.
40
+
41
+ The panel that holds the images is also `touch-action: pan-y` rather than `touch-action: none` now. It is an `overflow-y: auto` box, so on a phone it is the thing a finger has to drag when the images do not fit, and `none` told the browser not to pan it at all — which is what turned "some images are off-screen" into "some images cannot be reached". Stopping the page behind from scrolling is `overscroll-behavior`'s job and it still does it. That one is older than the randomisation; it came in with the React to vanilla rewrite, and the React component before it had the same thing.
42
+
43
+ Covered by a test that works out where three tiles and two gaps land for every gap the challenge draws, against a set of panel widths including the A52s' 345.578px. It fails on the old expression with exactly the numbers measured on the device.
44
+ - Updated dependencies [3d45c37]
45
+ - Updated dependencies [4cc28db]
46
+ - Updated dependencies [d3b3286]
47
+ - @prosopo/widget-skeleton@2.10.0
48
+ - @prosopo/procaptcha-common@2.17.1
49
+ - @prosopo/procaptcha@2.11.19
50
+
51
+ ## 2.13.0
52
+ ### Minor Changes
53
+
54
+ - 5e5fb9e: Give the image challenge dialog different class names, a different DOM shape, different element types for its controls and slightly different spacing on every page load, so a solver script cannot hardcode a way to find or click anything inside it.
55
+
56
+ This is the same treatment the checkbox got. Until now the dialog was identical for every user, forever: the panel was always `.prosopo-modalInner` inside `.prosopo-modalOuter` inside `.prosopo-challenge-content`, every tile and every action was a `<button>`, the grid gap was always 10px, and the whole thing sat at the same offsets. The end-to-end suite was itself finding the images by climbing four parents and stepping sideways from the instruction line — which is the clearest possible evidence that the markup was a reliable handle.
57
+
58
+ Four things now change, all drawn fresh each time a challenge mounts:
59
+
60
+ - **The names.** Every class the dialog renders — the two modal layers, the challenge surface and its panel, the reload control — is an 8-character token drawn at mount. Selectors scraped from one page load are dead on the next. The challenge surface is shared with the puzzle, so the puzzle gets this too. Nothing in the widget's own CSS referenced these names, but a site that had written its own rules against `.prosopo-modalOuter`, `.prosopo-modalInner`, `.prosopo-challenge-content` or `.reload-button` will find they no longer match. `image-captcha` on the widget root stays fixed, as `prosopo-checkbox` did, because that one sits in the embedding page's light DOM.
61
+ - **The shape.** The panel, the header, the grid, the action row, each action and each tile sit inside a random number of randomly named `div` or `span` wrappers. The wrappers are `display: contents`, so they generate no boxes and cannot move anything: depth varies, layout does not. Only elements with no accessible role are used, so nothing new is announced to a screen reader.
62
+ - **The element each control is made of.** Every tile and every action is now randomly either a real `<button>` or a `<div role="button" tabindex="0">`, so `button` no longer finds them all. The generic variant is given by hand what the button gave for free: it is in the tab order, it activates on Enter and on Space, Space does not scroll the dialog, and it carries the same `aria-pressed` state. The surface's focus trap already matched `[tabindex]:not([tabindex="-1"])`, so both variants are trapped and tabbable alike.
63
+ - **The spacing.** The grid gap, the grid's and the header's padding, the action row's padding and the panel's own margins are each nudged a few pixels, so tile centres are not where they were last time. Each of these changes the panel's size rather than moving the panel, so the surface still centres it and a panel taller than the viewport still scrolls to its own edges. The tile and action widths are now derived from the gap they were drawn with — three columns fit only if each cell gives up two thirds of a gap, and the old hardcoded basis would have wrapped the row at the wide end of the range.
64
+
65
+ Three related fixes come with it:
66
+
67
+ - The tick badge on a selected tile was shipping `data-testid="CheckIcon"` to production, plus an `aria-label` on an `aria-hidden` element that nothing could ever read and a stray `color="#fff"` attribute. All three are gone.
68
+ - The reload button acted on any click, including one a script dispatched. It now takes the same trusted-event gate every other control in the widget has.
69
+ - The end-to-end suite no longer depends on the markup at all. It uses `data-cy` hooks — the instruction line, the challenge panel, the round, the reload control — that are withheld from production builds, exactly as the checkbox withholds `captcha-checkbox`.
70
+
71
+ Note what this does not do. The dialog still says it is a dialog, the tiles still expose the button role and their pressed state, and the images still carry alt text. A script that looks for what these things mean rather than what they are called will still find them. That is the price of the challenge being usable with a screen reader, and it is the right price; the point here is to kill the cheap selector, not to claim the dialog is unfindable.
72
+
73
+ ### Patch Changes
74
+
75
+ - Updated dependencies [94929c3]
76
+ - Updated dependencies [a9141c3]
77
+ - Updated dependencies [a9141c3]
78
+ - Updated dependencies [a9141c3]
79
+ - Updated dependencies [e180281]
80
+ - Updated dependencies [5e5fb9e]
81
+ - @prosopo/procaptcha-common@2.17.0
82
+ - @prosopo/locale@3.6.0
83
+ - @prosopo/common@3.1.59
84
+ - @prosopo/types@5.10.0
85
+ - @prosopo/util@3.3.11
86
+ - @prosopo/widget-skeleton@2.9.0
87
+ - @prosopo/procaptcha@2.11.18
88
+
3
89
  ## 2.12.0
4
90
  ### Minor Changes
5
91
 
@@ -27,7 +27,10 @@ var mountButton = (container, initialProps) => {
27
27
  let props = initialProps;
28
28
  let hover = false;
29
29
  let focusVisible = false;
30
- const button = (0, _prosopo_procaptcha_common.createElement)("button");
30
+ const button = (0, _prosopo_procaptcha_common.createControl)(teardown, { onActivate: (event) => {
31
+ event.preventDefault();
32
+ props.onClick();
33
+ } });
31
34
  const buttonStyle = () => {
32
35
  const theme = "light" === props.themeColor ? _prosopo_widget_skeleton.lightTheme : _prosopo_widget_skeleton.darkTheme;
33
36
  const baseStyle = {
@@ -62,14 +65,16 @@ var mountButton = (container, initialProps) => {
62
65
  });
63
66
  button.textContent = props.text;
64
67
  };
65
- teardown.addEventListener(button, "mouseenter", () => {
66
- hover = true;
67
- render();
68
- });
69
- teardown.addEventListener(button, "mouseleave", () => {
70
- hover = false;
71
- render();
72
- });
68
+ if ((0, _prosopo_widget_skeleton.canHover)()) {
69
+ teardown.addEventListener(button, "mouseenter", () => {
70
+ hover = true;
71
+ render();
72
+ });
73
+ teardown.addEventListener(button, "mouseleave", () => {
74
+ hover = false;
75
+ render();
76
+ });
77
+ }
73
78
  teardown.addEventListener(button, "focus", () => {
74
79
  focusVisible = button.matches(":focus-visible");
75
80
  render();
@@ -78,11 +83,6 @@ var mountButton = (container, initialProps) => {
78
83
  focusVisible = false;
79
84
  render();
80
85
  });
81
- teardown.addEventListener(button, "click", (event) => {
82
- if (!(0, _prosopo_procaptcha_common.isEventTrusted)(event)) return;
83
- event.preventDefault();
84
- props.onClick();
85
- });
86
86
  render();
87
87
  container.appendChild(button);
88
88
  return {
@@ -17,7 +17,7 @@ var outerStyle = (theme) => ({
17
17
  borderRadius: theme.shape.card,
18
18
  backgroundColor: theme.palette.background.default,
19
19
  userSelect: "none",
20
- touchAction: "none",
20
+ touchAction: "pan-y",
21
21
  overscrollBehavior: "none"
22
22
  });
23
23
  var promptStyle = (theme) => ({
@@ -34,11 +34,22 @@ var targetStyle = (theme) => ({
34
34
  color: theme.palette.titleAccent,
35
35
  fontWeight: 700
36
36
  });
37
- var columnStyle = {
37
+ var columnStyle = (gap) => ({
38
38
  position: "relative",
39
39
  flexGrow: 1,
40
- flexBasis: "calc(33.333% - 10px)"
41
- };
40
+ flexBasis: (0, _prosopo_procaptcha_common.threeColumnBasis)(gap)
41
+ });
42
+ var SPACING_JITTER = 4;
43
+ var MIN_CONTROL_GAP = 6;
44
+ var MAX_CONTROL_GAP = 14;
45
+ var HEADER_PADDING_Y = 12;
46
+ var HEADER_PADDING_X = 14;
47
+ var HEADER_PADDING_JITTER = 3;
48
+ var MIN_WRAPPER_DEPTH = 0;
49
+ var MAX_WRAPPER_DEPTH = 2;
50
+ var decoy = (element) => (0, _prosopo_procaptcha_common.wrapRandomly)(element, MIN_WRAPPER_DEPTH, MAX_WRAPPER_DEPTH);
51
+ /** A spacing token moved by a few pixels, never below zero. */
52
+ var jittered = (base, jitter = SPACING_JITTER) => Math.max(0, base + (0, _prosopo_widget_skeleton.randomInt)(-jitter, jitter));
42
53
  var mountCaptchaComponent = (container, initialProps) => {
43
54
  let props = initialProps;
44
55
  const themeOf = (themeColor) => "light" === themeColor ? _prosopo_widget_skeleton.lightTheme : _prosopo_widget_skeleton.darkTheme;
@@ -46,26 +57,29 @@ var mountCaptchaComponent = (container, initialProps) => {
46
57
  const currentSolution = () => (0, _prosopo_util.at)(props.solutions, props.index, { noWrap: true });
47
58
  const isLastRound = () => props.index >= props.challenge.captchas.length - 1;
48
59
  const theme = themeOf(props.themeColor);
49
- const doubleSpacing = `${theme.spacing.unit * 2}px`;
50
- const fullSpacing = `${theme.spacing.unit}px`;
60
+ const controlGap = (0, _prosopo_widget_skeleton.randomInt)(MIN_CONTROL_GAP, MAX_CONTROL_GAP);
61
+ const column = columnStyle(controlGap);
51
62
  const targetLabel = (0, _prosopo_procaptcha_common.createElement)("span", { style: targetStyle(theme) });
52
- const promptText = (0, _prosopo_procaptcha_common.createElement)("p", { style: promptStyle(theme) });
63
+ const promptText = (0, _prosopo_procaptcha_common.createElement)("p", {
64
+ style: promptStyle(theme),
65
+ attributes: require_index.default({ dev: { cy: "captcha-prompt" } })
66
+ });
53
67
  const hintText = (0, _prosopo_procaptcha_common.createElement)("p", { style: hintStyle(theme) });
54
68
  const header = (0, _prosopo_procaptcha_common.createElement)("div", {
55
69
  style: {
56
- padding: "12px 14px",
70
+ padding: `${jittered(HEADER_PADDING_Y, HEADER_PADDING_JITTER)}px ${jittered(HEADER_PADDING_X, HEADER_PADDING_JITTER)}px`,
57
71
  fontFamily: theme.font.fontFamily
58
72
  },
59
- children: [promptText, hintText]
73
+ children: [decoy(promptText), decoy(hintText)]
60
74
  });
61
75
  const headerBar = (0, _prosopo_procaptcha_common.createElement)("div", {
62
76
  style: {
63
77
  backgroundColor: theme.palette.primaryContainer.main,
64
78
  borderRadius: theme.shape.header,
65
79
  width: "100%",
66
- marginTop: fullSpacing
80
+ marginTop: `${jittered(theme.spacing.unit)}px`
67
81
  },
68
- children: [header]
82
+ children: [decoy(header)]
69
83
  });
70
84
  const headerRow = (0, _prosopo_procaptcha_common.createElement)("div", {
71
85
  style: {
@@ -73,15 +87,15 @@ var mountCaptchaComponent = (container, initialProps) => {
73
87
  alignItems: "center",
74
88
  width: "100%"
75
89
  },
76
- children: [headerBar]
90
+ children: [decoy(headerBar)]
77
91
  });
78
92
  const gridHost = (0, _prosopo_procaptcha_common.createElement)("div", {
79
93
  style: { overflow: "hidden" },
80
94
  attributes: require_index.default({ dev: { cy: `captcha-${props.index}` } })
81
95
  });
82
- const cancelHost = (0, _prosopo_procaptcha_common.createElement)("div", { style: columnStyle });
83
- const reloadHost = (0, _prosopo_procaptcha_common.createElement)("div", { style: columnStyle });
84
- const nextHost = (0, _prosopo_procaptcha_common.createElement)("div", { style: columnStyle });
96
+ const cancelHost = (0, _prosopo_procaptcha_common.createElement)("div", { style: column });
97
+ const reloadHost = (0, _prosopo_procaptcha_common.createElement)("div", { style: column });
98
+ const nextHost = (0, _prosopo_procaptcha_common.createElement)("div", { style: column });
85
99
  const controls = (0, _prosopo_procaptcha_common.createElement)("div", {
86
100
  style: {
87
101
  width: "100%",
@@ -89,12 +103,12 @@ var mountCaptchaComponent = (container, initialProps) => {
89
103
  display: "flex",
90
104
  flexDirection: "row",
91
105
  justifyContent: "space-between",
92
- gap: "10px"
106
+ gap: `${controlGap}px`
93
107
  },
94
108
  children: [
95
- cancelHost,
96
- reloadHost,
97
- nextHost
109
+ decoy(cancelHost),
110
+ decoy(reloadHost),
111
+ decoy(nextHost)
98
112
  ]
99
113
  });
100
114
  const controlsRow = (0, _prosopo_procaptcha_common.createElement)("div", {
@@ -103,9 +117,9 @@ var mountCaptchaComponent = (container, initialProps) => {
103
117
  alignItems: "center",
104
118
  justifyContent: "space-between",
105
119
  lineHeight: 1.75,
106
- padding: `${fullSpacing} 0 ${doubleSpacing} 0`
120
+ padding: `${jittered(theme.spacing.unit)}px 0 ${jittered(theme.spacing.unit * 2)}px 0`
107
121
  },
108
- children: [controls]
122
+ children: [decoy(controls)]
109
123
  });
110
124
  const inner = (0, _prosopo_procaptcha_common.createElement)("div", {
111
125
  style: {
@@ -113,19 +127,20 @@ var mountCaptchaComponent = (container, initialProps) => {
113
127
  display: "flex",
114
128
  flexDirection: "column",
115
129
  minWidth: "300px",
116
- marginLeft: fullSpacing,
117
- marginRight: fullSpacing,
130
+ marginLeft: `${jittered(theme.spacing.unit)}px`,
131
+ marginRight: `${jittered(theme.spacing.unit)}px`,
118
132
  justifyContent: "center"
119
133
  },
120
134
  children: [
121
- headerRow,
122
- gridHost,
123
- controlsRow
135
+ decoy(headerRow),
136
+ decoy(gridHost),
137
+ decoy(controlsRow)
124
138
  ]
125
139
  });
126
140
  const root = (0, _prosopo_procaptcha_common.createElement)("div", {
127
141
  style: outerStyle(theme),
128
- children: [inner]
142
+ attributes: require_index.default({ dev: { cy: "captcha-panel" } }),
143
+ children: [decoy(inner)]
129
144
  });
130
145
  const grid = require_captchaWidget.mountCaptchaWidget(gridHost, {
131
146
  challenge: currentCaptcha(),
@@ -6,23 +6,41 @@ var getHash = (item) => {
6
6
  if (!item.hash) throw new _prosopo_common.ProsopoDatasetError("CAPTCHA.MISSING_ITEM_HASH", { context: { item } });
7
7
  return item.hash;
8
8
  };
9
- var imageStyle = {
9
+ var cellStyle = (gap) => ({
10
10
  flexGrow: 1,
11
- flexBasis: "calc(33.333% - 10px)",
11
+ flexBasis: (0, _prosopo_procaptcha_common.threeColumnBasis)(gap),
12
12
  boxSizing: "border-box"
13
- };
13
+ });
14
+ var MIN_GRID_GAP = 8;
15
+ var MAX_GRID_GAP = 15;
16
+ var GRID_PADDING_JITTER = 4;
17
+ var MIN_WRAPPER_DEPTH = 0;
18
+ var MAX_WRAPPER_DEPTH = 2;
14
19
  var CHECK_ICON_PATH = "M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z";
20
+ /**
21
+ * Asset signing is optional, so both shapes of URL arrive here. An unsigned one
22
+ * gets the cache-buster it always got; a signed one cannot, because its token
23
+ * is a signature over the query string and any addition invalidates it —
24
+ * turning a transient failure into a permanent 403.
25
+ */
26
+ var retrySrc = (url) => url.includes("token=") ? url : `${url}${url.includes("?") ? "&" : "?"}retry=${Date.now()}`;
15
27
  var themeOf = (themeColor) => "light" === themeColor ? _prosopo_widget_skeleton.lightTheme : _prosopo_widget_skeleton.darkTheme;
16
- var gridStyle = (theme) => ({
28
+ /** Drawn once per mounted grid, so tile centres move between page loads. */
29
+ var generateMetrics = (theme) => ({
30
+ gap: (0, _prosopo_widget_skeleton.randomInt)(MIN_GRID_GAP, MAX_GRID_GAP),
31
+ paddingTop: Math.max(0, theme.spacing.unit + (0, _prosopo_widget_skeleton.randomInt)(-GRID_PADDING_JITTER, GRID_PADDING_JITTER)),
32
+ paddingBottom: Math.max(0, theme.spacing.unit + (0, _prosopo_widget_skeleton.randomInt)(-GRID_PADDING_JITTER, GRID_PADDING_JITTER))
33
+ });
34
+ var gridStyle = (metrics) => ({
17
35
  width: "100%",
18
36
  height: "100%",
19
37
  display: "flex",
20
38
  flexDirection: "row",
21
39
  flexWrap: "wrap",
22
40
  justifyContent: "space-between",
23
- paddingTop: `${theme.spacing.unit}px`,
24
- paddingBottom: `${theme.spacing.unit}px`,
25
- gap: "10px"
41
+ paddingTop: `${metrics.paddingTop}px`,
42
+ paddingBottom: `${metrics.paddingBottom}px`,
43
+ gap: `${metrics.gap}px`
26
44
  });
27
45
  var selectionStyle = (theme, selected) => ({
28
46
  borderRadius: selected ? theme.shape.tileSelected : theme.shape.tile,
@@ -42,7 +60,8 @@ var mountCaptchaWidget = (container, initialProps) => {
42
60
  let renderedChallenge;
43
61
  let tiles = [];
44
62
  let focusedHash = null;
45
- const grid = (0, _prosopo_procaptcha_common.createElement)("div", { style: gridStyle(themeOf(initialProps.themeColor)) });
63
+ const metrics = generateMetrics(themeOf(initialProps.themeColor));
64
+ const grid = (0, _prosopo_procaptcha_common.createElement)("div", { style: gridStyle(metrics) });
46
65
  const buildTile = (item, index) => {
47
66
  const hash = getHash(item);
48
67
  const theme = themeOf(props.themeColor);
@@ -67,7 +86,10 @@ var mountCaptchaWidget = (container, initialProps) => {
67
86
  teardown.addEventListener(image, "error", () => {
68
87
  const retryCount = Number(image.dataset.retryCount ?? "0") + 1;
69
88
  image.dataset.retryCount = String(retryCount);
70
- if (retryCount <= 3) image.src = `${item.data}?retry=${Date.now()}`;
89
+ if (retryCount <= 3) {
90
+ image.removeAttribute("src");
91
+ image.src = retrySrc(item.data);
92
+ }
71
93
  });
72
94
  const icon = (0, _prosopo_procaptcha_common.createSvgElement)("svg", {
73
95
  style: {
@@ -84,11 +106,8 @@ var mountCaptchaWidget = (container, initialProps) => {
84
106
  },
85
107
  attributes: {
86
108
  focusable: "false",
87
- color: "#fff",
88
109
  "aria-hidden": "true",
89
- viewBox: "0 0 24 24",
90
- "data-testid": "CheckIcon",
91
- "aria-label": "Check icon"
110
+ viewBox: "0 0 24 24"
92
111
  },
93
112
  children: [(0, _prosopo_procaptcha_common.createSvgElement)("path", { attributes: { d: CHECK_ICON_PATH } })]
94
113
  });
@@ -110,7 +129,7 @@ var mountCaptchaWidget = (container, initialProps) => {
110
129
  },
111
130
  children: [icon]
112
131
  });
113
- const clickable = (0, _prosopo_procaptcha_common.createElement)("button", {
132
+ const clickable = (0, _prosopo_procaptcha_common.createControl)(teardown, {
114
133
  style: {
115
134
  position: "relative",
116
135
  cursor: "pointer",
@@ -123,13 +142,11 @@ var mountCaptchaWidget = (container, initialProps) => {
123
142
  appearance: "none",
124
143
  display: "block"
125
144
  },
126
- attributes: { type: "button" },
127
- children: [image, overlay]
128
- });
129
- teardown.addEventListener(clickable, "click", (event) => {
130
- if (!(0, _prosopo_procaptcha_common.isEventTrusted)(event)) return;
131
- const mouseEvent = event;
132
- props.onClick(hash, mouseEvent.clientX, mouseEvent.clientY);
145
+ children: [image, overlay],
146
+ onActivate: (event) => {
147
+ const fromPointer = "clientX" in event;
148
+ props.onClick(hash, fromPointer ? event.clientX : 0, fromPointer ? event.clientY : 0);
149
+ }
133
150
  });
134
151
  teardown.addEventListener(clickable, "focus", () => {
135
152
  focusedHash = clickable.matches(":focus-visible") ? hash : null;
@@ -140,10 +157,10 @@ var mountCaptchaWidget = (container, initialProps) => {
140
157
  applyFocusRing();
141
158
  });
142
159
  const cell = (0, _prosopo_procaptcha_common.createElement)("div", {
143
- style: imageStyle,
160
+ style: cellStyle(metrics.gap),
144
161
  children: [clickable]
145
162
  });
146
- grid.appendChild(cell);
163
+ grid.appendChild((0, _prosopo_procaptcha_common.wrapRandomly)(cell, MIN_WRAPPER_DEPTH, MAX_WRAPPER_DEPTH));
147
164
  return {
148
165
  hash,
149
166
  image,
@@ -165,7 +182,7 @@ var mountCaptchaWidget = (container, initialProps) => {
165
182
  teardown.run();
166
183
  teardown = new _prosopo_procaptcha_common.Teardown();
167
184
  (0, _prosopo_procaptcha_common.clearElement)(grid);
168
- (0, _prosopo_procaptcha_common.applyStyles)(grid, gridStyle(themeOf(props.themeColor)));
185
+ (0, _prosopo_procaptcha_common.applyStyles)(grid, gridStyle(metrics));
169
186
  tiles = props.challenge.items.map(buildTile);
170
187
  renderedChallenge = props.challenge;
171
188
  };
@@ -1,10 +1,13 @@
1
1
  let _prosopo_procaptcha_common = require("@prosopo/procaptcha-common");
2
+ let _prosopo_widget_skeleton = require("@prosopo/widget-skeleton");
2
3
  //#region src/components/modal.ts
4
+ var MIN_WRAPPER_DEPTH = 1;
5
+ var MAX_WRAPPER_DEPTH = 3;
3
6
  /**
4
7
  * The image captcha's dialog frame; positioning lives in `ChallengeSurface`.
5
8
  */
6
9
  var mountModal = (initialProps) => {
7
- const inner = (0, _prosopo_procaptcha_common.createElement)("div", { className: "prosopo-modalInner" });
10
+ const inner = (0, _prosopo_procaptcha_common.createElement)("div", { className: (0, _prosopo_widget_skeleton.randomToken)() });
8
11
  (0, _prosopo_procaptcha_common.applyStyles)(inner, {
9
12
  maxWidth: "500px",
10
13
  maxHeight: "100%",
@@ -14,17 +17,18 @@ var mountModal = (initialProps) => {
14
17
  alignSelf: "center",
15
18
  boxSizing: "border-box"
16
19
  });
20
+ const outerClassName = (0, _prosopo_widget_skeleton.randomToken)();
17
21
  const surfaceProps = (props) => ({
18
22
  show: props.show,
19
23
  placement: props.placement,
20
24
  anchor: props.anchor,
21
25
  onDismiss: props.onDismiss,
22
26
  scrim: "none",
23
- className: "prosopo-modalOuter",
27
+ className: outerClassName,
24
28
  dialogLabel: props.dialogLabel
25
29
  });
26
30
  const surface = (0, _prosopo_procaptcha_common.mountChallengeSurface)(surfaceProps(initialProps));
27
- surface.content.appendChild(inner);
31
+ surface.content.appendChild((0, _prosopo_procaptcha_common.wrapRandomly)(inner, MIN_WRAPPER_DEPTH, MAX_WRAPPER_DEPTH));
28
32
  return {
29
33
  content: inner,
30
34
  update: (props) => surface.update(surfaceProps(props)),