@prosopo/client-bundle-example 2.10.22 → 2.10.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build$colon$cjs.log +2 -2
- package/.turbo/turbo-build$colon$tsc.log +11 -11
- package/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +51 -0
- package/package.json +9 -9
- package/src/frictionless-explicit.html +8 -0
- package/src/frictionless-manual-start.html +224 -0
- package/src/image-explicit.html +8 -0
- package/src/plugins/navigation-injector.ts +2 -0
- package/src/plugins/placement-injector.ts +153 -0
- package/src/pow-implicit-sessionid.html +223 -0
- package/src/puzzle-bind-explicit.html +98 -0
- package/src/puzzle-explicit.html +8 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.config.ts +10 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @prosopo/client-bundle-example@2.10.
|
|
2
|
+
> @prosopo/client-bundle-example@2.10.24 build:cjs
|
|
3
3
|
> NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV
|
|
4
4
|
|
|
5
5
|
ViteCommonJSConfig: .
|
|
@@ -32,4 +32,4 @@ rendering chunks...
|
|
|
32
32
|
computing gzip size...
|
|
33
33
|
dist/cjs/index.cjs 1.41 kB │ gzip: 0.64 kB
|
|
34
34
|
|
|
35
|
-
[32m✓ built in
|
|
35
|
+
[32m✓ built in 51ms[39m
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
|
-
> @prosopo/client-bundle-example@2.10.
|
|
2
|
+
> @prosopo/client-bundle-example@2.10.24 build:tsc
|
|
3
3
|
> tsc --build --verbose
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
1:04:08 PM - Projects in this build:
|
|
6
6
|
* ../../dev/config/tsconfig.json
|
|
7
7
|
* ../../packages/util/tsconfig.json
|
|
8
8
|
* ../../packages/logger/tsconfig.json
|
|
@@ -12,21 +12,21 @@
|
|
|
12
12
|
* ../../packages/types/tsconfig.json
|
|
13
13
|
* tsconfig.json
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
1:04:08 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'
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
1:04:08 PM - Project '../../packages/util/tsconfig.json' is up to date because newest input '../../packages/util/src/url.ts' is older than output '../../packages/util/tsconfig.tsbuildinfo'
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
1:04:08 PM - Project '../../packages/logger/tsconfig.json' is up to date because newest input '../../packages/logger/src/logger.ts' is older than output '../../packages/logger/tsconfig.tsbuildinfo'
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
1:04:08 PM - Project '../../packages/dotenv/tsconfig.json' is up to date because newest input '../../packages/dotenv/src/env.ts' is older than output '../../packages/dotenv/tsconfig.tsbuildinfo'
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
1:04:08 PM - Project '../../packages/locale/tsconfig.json' is up to date because newest input '../../packages/locale/src/translationKey.ts' is older than output '../../packages/locale/tsconfig.tsbuildinfo'
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
1:04:08 PM - Project '../../packages/util-crypto/tsconfig.json' is up to date because newest input '../../packages/util-crypto/src/types.ts' is older than output '../../packages/util-crypto/tsconfig.tsbuildinfo'
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
1:04:08 PM - Project '../../packages/types/tsconfig.json' is up to date because newest input '../../packages/types/src/types.test-d.ts' is older than output '../../packages/types/tsconfig.tsbuildinfo'
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
1:04:08 PM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
1:04:08 PM - Building project '/home/runner/work/captcha/captcha/demos/client-bundle-example/tsconfig.json'...
|
|
32
32
|
|
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
|
-
> @prosopo/client-bundle-example@2.10.
|
|
2
|
+
> @prosopo/client-bundle-example@2.10.24 build
|
|
3
3
|
> npm run build:cross-env -- --mode ${NODE_ENV:-development}
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
> @prosopo/client-bundle-example@2.10.
|
|
6
|
+
> @prosopo/client-bundle-example@2.10.24 build:cross-env
|
|
7
7
|
> vite build --config vite.esm.config.ts --mode production
|
|
8
8
|
|
|
9
9
|
ViteEsmConfig: .
|
|
@@ -37,4 +37,4 @@ computing gzip size...
|
|
|
37
37
|
dist/_virtual/_rolldown/runtime.js 0.04 kB │ gzip: 0.06 kB
|
|
38
38
|
dist/index.js 1.47 kB │ gzip: 0.68 kB
|
|
39
39
|
|
|
40
|
-
[32m✓ built in
|
|
40
|
+
[32m✓ built in 124ms[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @prosopo/client-bundle-example
|
|
2
2
|
|
|
3
|
+
## 2.10.24
|
|
4
|
+
### Patch Changes
|
|
5
|
+
|
|
6
|
+
- f4e4a83: chore(deps): roll up the open dependabot bumps (react 19.3, mongoose 9.10, @polkadot/util 14, redis 6, cron-parser 5, react-i18next 17 with i18next 26, @scure/base 2, cypress 16, rollup/babel plugin majors, vitest 4.1.11, angular 20.3.28, js-yaml)
|
|
7
|
+
|
|
8
|
+
## 2.10.23
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 89dd38a: chore(deps): batch the outstanding dependabot bumps into one upgrade
|
|
12
|
+
|
|
13
|
+
Rolls up dependabot PRs #3112, #3127-#3134 and #3159. Majors: `mongoose`
|
|
14
|
+
8 -> 9, `bson` 6 -> 7, `@noble/curves` 1 -> 2, `@polkadot/util-crypto`
|
|
15
|
+
13 -> 14, `@typegoose/auto-increment` 4 -> 5, `@babel/preset-env` 7 -> 8,
|
|
16
|
+
`@types/jsdom` 21 -> 30, `@types/bcrypt` 5 -> 6, `@actions/github` 6 -> 9,
|
|
17
|
+
`testcontainers` 11 -> 12. The rest are minor/patch.
|
|
18
|
+
|
|
19
|
+
Code changes the majors forced:
|
|
20
|
+
- `@noble/curves` v2 requires `.js` specifiers and renamed the point API,
|
|
21
|
+
so `secp256k1.ProjectivePoint.fromHex(...).toRawBytes()` becomes
|
|
22
|
+
`secp256k1.Point.fromBytes(...).toBytes()`, `RistrettoPoint` becomes
|
|
23
|
+
`ristretto255.Point`, and `abstract/utils` moves to `utils.js`.
|
|
24
|
+
- mongoose 9 drops `RootFilterQuery` (now `QueryFilter`), no longer sets
|
|
25
|
+
`background: true` on schema indexes by default, and no longer declares
|
|
26
|
+
`id` on `Document`, which un-hid a mismatch between
|
|
27
|
+
`updateDappUserCommitment`'s `Hash` parameter and the `string` `id` it
|
|
28
|
+
filters on.
|
|
29
|
+
- mongoose 9 rejects an aggregation-pipeline update (an array) unless the
|
|
30
|
+
call passes `updatePipeline: true`, so the six pipeline writes in
|
|
31
|
+
`ProviderDatabase` now opt in explicitly.
|
|
32
|
+
- mongoose 9's `castUpdate` throws on a `$setOnInsert` key inside `$set`.
|
|
33
|
+
`storeUserImageCaptchaSolution` passed its record straight in as the
|
|
34
|
+
update, and mongoose's `moveImmutableProperties` mutates that object on
|
|
35
|
+
an upsert -- adding the very `$setOnInsert` key the record then carried
|
|
36
|
+
into `CentralDbStreamer.streamImageRecord`. Image records stopped
|
|
37
|
+
reaching the central DB (the streamer is fire-and-forget, so it only
|
|
38
|
+
logged) and signup verification returned 500. The update is now an
|
|
39
|
+
explicit `$set` over a shallow copy.
|
|
40
|
+
- `@prosopo/database` moves from mongodb 6.20 to 7.5 to match the driver
|
|
41
|
+
mongoose 9 pulls, so bson 7 is the only copy resolvable in the package.
|
|
42
|
+
- `vitest`/`@vitest/coverage-v8` go to 4.1.11 alongside dependabot's
|
|
43
|
+
`@vitest/spy` bump; leaving them at 4.1.10 installed a second copy of
|
|
44
|
+
`@vitest/spy` and broke type inference in the provider test utils.
|
|
45
|
+
- 80f73c1: Sites can now control when the widget starts working.
|
|
46
|
+
|
|
47
|
+
By default the widget runs bot detection, starts the behavioural collectors and calls `/frictionless` as soon as it mounts. Rendering with `data-start-mode="manual"` (or `startMode: "manual"` in the render options) keeps all of that off the page load: the checkbox still appears immediately, at its final size, so nothing shifts, but the widget does nothing else until one of two things happens.
|
|
48
|
+
|
|
49
|
+
- The site calls `window.procaptcha.start()`, optionally with a widget id, or dispatches a `procaptcha:start` event on `document`. The frictionless flow runs and the widget then waits for a click exactly as it does today.
|
|
50
|
+
- The visitor clicks the checkbox. The frictionless flow runs and whichever challenge the provider chooses opens straight away, carrying that click's position, so the visitor is never asked to click twice.
|
|
51
|
+
|
|
52
|
+
Both triggers are one-shot: whichever comes first wins and the other is ignored. `window.procaptcha.execute()` also starts a manual widget, opening its challenge immediately. Widgets in the default `auto` mode are unaffected.
|
|
53
|
+
|
|
3
54
|
## 2.10.22
|
|
4
55
|
### Patch Changes
|
|
5
56
|
|
package/package.json
CHANGED
|
@@ -25,26 +25,26 @@
|
|
|
25
25
|
"start": "NODE_ENV=${NODE_ENV:-development}; vite serve ./src --port 9232 --config vite.config.ts --mode=$NODE_ENV",
|
|
26
26
|
"test": "NODE_ENV=${NODE_ENV:-test}; npx vitest run --config ./vite.test.config.ts"
|
|
27
27
|
},
|
|
28
|
-
"version": "2.10.
|
|
28
|
+
"version": "2.10.24",
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@emotion/cache": "11.14.0",
|
|
31
|
-
"@prosopo/config": "3.3.
|
|
32
|
-
"@prosopo/dotenv": "3.0.
|
|
33
|
-
"@prosopo/locale": "3.3
|
|
34
|
-
"@prosopo/types": "5.
|
|
31
|
+
"@prosopo/config": "3.3.17",
|
|
32
|
+
"@prosopo/dotenv": "3.0.56",
|
|
33
|
+
"@prosopo/locale": "3.4.3",
|
|
34
|
+
"@prosopo/types": "5.8.4",
|
|
35
35
|
"@types/node": "22.5.5",
|
|
36
|
-
"@vitest/coverage-v8": "4.1.
|
|
36
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
37
37
|
"concurrently": "9.0.1",
|
|
38
38
|
"del-cli": "6.0.0",
|
|
39
39
|
"jsdom": "25.0.0",
|
|
40
40
|
"npm-run-all": "4.1.5",
|
|
41
|
-
"react": "19.
|
|
42
|
-
"react-dom": "19.
|
|
41
|
+
"react": "19.3.0",
|
|
42
|
+
"react-dom": "19.3.0",
|
|
43
43
|
"tslib": "2.7.0",
|
|
44
44
|
"tsx": "4.20.3",
|
|
45
45
|
"typescript": "5.6.2",
|
|
46
46
|
"vite": "8.1.5",
|
|
47
|
-
"vitest": "4.1.
|
|
47
|
+
"vitest": "4.1.11"
|
|
48
48
|
},
|
|
49
49
|
"repository": {
|
|
50
50
|
"type": "git",
|
|
@@ -193,9 +193,17 @@
|
|
|
193
193
|
// Get the Element using elementId
|
|
194
194
|
const captchaContainer = document.getElementById('procaptcha-container');
|
|
195
195
|
|
|
196
|
+
// ?placement=float anchors the challenge to the widget; the default is popup.
|
|
197
|
+
const placement =
|
|
198
|
+
new URLSearchParams(window.location.search).get("placement") === "float"
|
|
199
|
+
? "float"
|
|
200
|
+
: "popup";
|
|
201
|
+
window.updateCaptchaStatus('Challenge placement: ' + placement, 'info');
|
|
202
|
+
|
|
196
203
|
// Render the CAPTCHA
|
|
197
204
|
const widgetId = render(captchaContainer, {
|
|
198
205
|
siteKey: import.meta.env.PROSOPO_SITE_KEY_FRICTIONLESS,
|
|
206
|
+
placement,
|
|
199
207
|
callback: onCaptchaVerified,
|
|
200
208
|
"failed-callback": function () {
|
|
201
209
|
console.log('Challenge failed');
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<link href="https://cdn.muicss.com/mui-0.10.3/css/mui.min.css" rel="stylesheet" type="text/css"/>
|
|
5
|
+
<link href="styles/field.css" rel="stylesheet" type="text/css"/>
|
|
6
|
+
|
|
7
|
+
<script src="index.js"></script>
|
|
8
|
+
<title>Procaptcha demo: Frictionless - Manual Start</title>
|
|
9
|
+
<link href="styles/captcha.css" rel="stylesheet" type="text/css"/>
|
|
10
|
+
<script type="module">
|
|
11
|
+
// Function to update CAPTCHA status display is now provided by status-log-injector
|
|
12
|
+
|
|
13
|
+
window.onStartCaptcha = () => {
|
|
14
|
+
updateCaptchaStatus('window.procaptcha.start() called - frictionless flow starting', 'info');
|
|
15
|
+
window.procaptcha.start();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
window.addEventListener('load', () => {
|
|
19
|
+
|
|
20
|
+
window.setIsError = (isError) => {
|
|
21
|
+
const messageContainer = document.getElementById('messageContainer');
|
|
22
|
+
messageContainer.style.color = isError ? 'red' : 'black';
|
|
23
|
+
messageContainer.style.display = 'block';
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
window.setMessage = (message) => {
|
|
27
|
+
document.getElementById('message').innerText = message;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
window.onLoggedIn = (token) => {
|
|
31
|
+
updateCaptchaStatus('Logged in, fetching private resource', 'info');
|
|
32
|
+
const url = new URL("/private", config.serverUrl).href;
|
|
33
|
+
console.log("getting private resource with token ", token, "at", url);
|
|
34
|
+
fetch(url, {
|
|
35
|
+
method: "GET",
|
|
36
|
+
headers: {
|
|
37
|
+
Origin: "http://localhost:9232", // TODO: change this to env var
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
Authorization: `Bearer ${token}`,
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
.then(async (res) => {
|
|
43
|
+
try {
|
|
44
|
+
const jsonRes = await res.json();
|
|
45
|
+
if (res.status === 200) {
|
|
46
|
+
updateCaptchaStatus(`Successfully accessed private resource`, 'success');
|
|
47
|
+
setMessage(jsonRes.message);
|
|
48
|
+
} else {
|
|
49
|
+
updateCaptchaStatus(`Failed to access private resource: ${res.status}`, 'error');
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.log(err);
|
|
53
|
+
updateCaptchaStatus(`Error parsing response: ${err}`, 'error');
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.catch((err) => {
|
|
57
|
+
console.log(err);
|
|
58
|
+
updateCaptchaStatus(`Fetch error: ${err}`, 'error');
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
window.onActionHandler = () => {
|
|
63
|
+
updateCaptchaStatus('Form submission initiated', 'info');
|
|
64
|
+
|
|
65
|
+
const procaptchaElements = document.getElementsByName('procaptcha-response');
|
|
66
|
+
|
|
67
|
+
if (!procaptchaElements.length) {
|
|
68
|
+
updateCaptchaStatus('Error: No CAPTCHA response found', 'error');
|
|
69
|
+
alert("Must complete captcha");
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const procaptchaToken = procaptchaElements[0].value;
|
|
74
|
+
updateCaptchaStatus(`Token received: ${procaptchaToken.substring(0, 15)}...`, 'success');
|
|
75
|
+
|
|
76
|
+
const payload = {
|
|
77
|
+
email: document.getElementById('email').value,
|
|
78
|
+
name: document.getElementById('name').value,
|
|
79
|
+
password: document.getElementById('password').value,
|
|
80
|
+
siteKey: import.meta.env.PROSOPO_SITE_KEY_FRICTIONLESS,
|
|
81
|
+
"procaptcha-response": procaptchaToken,
|
|
82
|
+
};
|
|
83
|
+
const url = new URL('signup', import.meta.env.PROSOPO_SERVER_URL).href;
|
|
84
|
+
console.log("posting to", url, "with payload", payload);
|
|
85
|
+
fetch(url, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify(payload),
|
|
91
|
+
contentType: "application/json",
|
|
92
|
+
}).then((response) => {
|
|
93
|
+
return new Promise((resolve) => response.json()
|
|
94
|
+
.then((json) => resolve({
|
|
95
|
+
status: response.status,
|
|
96
|
+
ok: response.ok,
|
|
97
|
+
json,
|
|
98
|
+
})))
|
|
99
|
+
})
|
|
100
|
+
.then(async ({status, json, ok}) => {
|
|
101
|
+
console.log("status", status, "json", json.message, "ok", ok);
|
|
102
|
+
console.log("json", json)
|
|
103
|
+
try {
|
|
104
|
+
if (status !== 200) {
|
|
105
|
+
updateCaptchaStatus(`API Error: ${json.message}`, 'error');
|
|
106
|
+
setIsError(true);
|
|
107
|
+
setMessage(json.message);
|
|
108
|
+
} else {
|
|
109
|
+
updateCaptchaStatus(`API Success: ${json.message}`, 'success');
|
|
110
|
+
setIsError(false);
|
|
111
|
+
setMessage(json.message);
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.log(err);
|
|
115
|
+
updateCaptchaStatus(`Error processing response: ${err}`, 'error');
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
.catch((err) => {
|
|
119
|
+
console.log(err);
|
|
120
|
+
updateCaptchaStatus(`Fetch error: ${err}`, 'error');
|
|
121
|
+
setIsError(true);
|
|
122
|
+
setMessage("Error: " + err);
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
updateCaptchaStatus('Page loaded - Initializing CAPTCHA system', 'info');
|
|
127
|
+
|
|
128
|
+
// Monitor DOM for CAPTCHA initialization
|
|
129
|
+
const observer = new MutationObserver((mutations) => {
|
|
130
|
+
mutations.forEach((mutation) => {
|
|
131
|
+
if (mutation.addedNodes.length) {
|
|
132
|
+
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
|
133
|
+
const node = mutation.addedNodes[i];
|
|
134
|
+
if (node.classList && (node.classList.contains('procaptcha') ||
|
|
135
|
+
node.querySelector && node.querySelector('.procaptcha'))) {
|
|
136
|
+
updateCaptchaStatus('CAPTCHA DOM elements initialized', 'info');
|
|
137
|
+
observer.disconnect();
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
window.onCaptchaFailed = function () {
|
|
150
|
+
console.log('Challenge failed');
|
|
151
|
+
updateCaptchaStatus('Challenge failed - CAPTCHA verification could not be completed', 'error');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
window.onCaptchaVerified = (output) => {
|
|
155
|
+
console.log('Challenge passed');
|
|
156
|
+
updateCaptchaStatus('Challenge passed successfully!', 'success');
|
|
157
|
+
updateCaptchaStatus(`Token generated: ${output.substring(0, 15)}...`, 'success');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Add custom event listener for CAPTCHA verification stages
|
|
161
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
updateCaptchaStatus('CAPTCHA script loaded and initialized', 'info');
|
|
164
|
+
updateCaptchaStatus('Waiting for user interaction...', 'info');
|
|
165
|
+
}, 500);
|
|
166
|
+
});
|
|
167
|
+
</script>
|
|
168
|
+
<script id="procaptchaScript" type="module" src="%VITE_BUNDLE_URL%" async defer></script>
|
|
169
|
+
</head>
|
|
170
|
+
<body>
|
|
171
|
+
|
|
172
|
+
<div class="mui-container">
|
|
173
|
+
<h1>Frictionless CAPTCHA - Manual Start</h1>
|
|
174
|
+
<p>
|
|
175
|
+
This example renders the widget with <code>data-start-mode="manual"</code>. The checkbox appears
|
|
176
|
+
immediately at its final size, but no bot detection, behavioural collection or provider request runs
|
|
177
|
+
until either the page calls <code>window.procaptcha.start()</code> (the button below) or the visitor
|
|
178
|
+
clicks the checkbox. A click that starts the flow also opens whichever challenge the provider picks,
|
|
179
|
+
so the visitor never has to click twice.
|
|
180
|
+
</p>
|
|
181
|
+
|
|
182
|
+
<!-- CAPTCHA Status Display will be injected by the status-log-injector plugin -->
|
|
183
|
+
|
|
184
|
+
<form action="%PROSOPO_SERVER_URL%/signup" method="POST" class="mui-form">
|
|
185
|
+
<h2>Example Login Form</h2>
|
|
186
|
+
<div class="mui-textfield mui-textfield--float-label">
|
|
187
|
+
<label for="name">Name</label>
|
|
188
|
+
<input id="name" type="text" name="name" required/>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="mui-textfield mui-textfield--float-label">
|
|
191
|
+
<label for="email">Email Address</label>
|
|
192
|
+
<input id="email" type="email" name="email" required/>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="mui-textfield mui-textfield--float-label">
|
|
195
|
+
<label for="password">Password</label>
|
|
196
|
+
<input id="password" type="password" name="password" required/>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="mui-textfield mui-textfield--float-label">
|
|
199
|
+
<!-- Dev sitekey -->
|
|
200
|
+
<div
|
|
201
|
+
class="procaptcha"
|
|
202
|
+
data-theme="light"
|
|
203
|
+
data-sitekey="%PROSOPO_SITE_KEY_FRICTIONLESS%"
|
|
204
|
+
data-start-mode="manual"
|
|
205
|
+
data-failed-callback="onCaptchaFailed"
|
|
206
|
+
data-callback="onCaptchaVerified"
|
|
207
|
+
></div>
|
|
208
|
+
</div>
|
|
209
|
+
<div class="mui-textfield">
|
|
210
|
+
<button type="button" class="mui-btn mui-btn--flat" onclick="onStartCaptcha()" data-cy="start-captcha-button">
|
|
211
|
+
Start CAPTCHA now
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
<div id="messageContainer" style="display: none; color: black;"><span id="message"></span></div>
|
|
215
|
+
<button type="button" class="mui-btn mui-btn--raised" onclick="onActionHandler()" data-cy="submit-button">Submit</button>
|
|
216
|
+
</form>
|
|
217
|
+
|
|
218
|
+
<!-- Console output display area -->
|
|
219
|
+
<div id="console-output" class="console-output" style="display: none;"></div>
|
|
220
|
+
|
|
221
|
+
<!-- Explanation will be injected by the explanation-injector plugin -->
|
|
222
|
+
</div>
|
|
223
|
+
</body>
|
|
224
|
+
</html>
|
package/src/image-explicit.html
CHANGED
|
@@ -135,8 +135,16 @@
|
|
|
135
135
|
window.updateCaptchaStatus('Initializing CAPTCHA with explicit render call', 'info');
|
|
136
136
|
|
|
137
137
|
// Render the CAPTCHA
|
|
138
|
+
// ?placement=float anchors the challenge to the widget; the default is popup.
|
|
139
|
+
const placement =
|
|
140
|
+
new URLSearchParams(window.location.search).get("placement") === "float"
|
|
141
|
+
? "float"
|
|
142
|
+
: "popup";
|
|
143
|
+
window.updateCaptchaStatus('Challenge placement: ' + placement, 'info');
|
|
144
|
+
|
|
138
145
|
const widgetId = render(document.getElementById('procaptcha-container'), {
|
|
139
146
|
siteKey: import.meta.env.PROSOPO_SITE_KEY_IMAGE,
|
|
147
|
+
placement,
|
|
140
148
|
callback: handleCaptchaResponse,
|
|
141
149
|
"failed-callback": handleCaptchaFailed
|
|
142
150
|
});
|
|
@@ -47,10 +47,12 @@ export default function navigationInjector(): Plugin {
|
|
|
47
47
|
frictionless: {
|
|
48
48
|
implicit: { path: "frictionless-implicit.html", exists: true },
|
|
49
49
|
explicit: { path: "frictionless-explicit.html", exists: true },
|
|
50
|
+
manual: { path: "frictionless-manual-start.html", exists: true },
|
|
50
51
|
},
|
|
51
52
|
puzzle: {
|
|
52
53
|
implicit: { path: "puzzle-implicit.html", exists: true },
|
|
53
54
|
explicit: { path: "puzzle-explicit.html", exists: true },
|
|
55
|
+
bound: { path: "puzzle-bind-explicit.html", exists: true },
|
|
54
56
|
},
|
|
55
57
|
},
|
|
56
58
|
invisible: {
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
// Vite plugin to inject a popup/float placement switcher into HTML files.
|
|
15
|
+
//
|
|
16
|
+
// Placement is resolved once, when the widget renders, so switching it means
|
|
17
|
+
// re-rendering. The switcher therefore drives `?placement=` and reloads rather
|
|
18
|
+
// than mutating a live widget. The explicit pages read that query parameter
|
|
19
|
+
// themselves and pass `placement` as a render option; implicit pages have no
|
|
20
|
+
// script of their own, so the injected script stamps `data-placement` onto
|
|
21
|
+
// their containers instead, which exercises the attribute path.
|
|
22
|
+
import type { IndexHtmlTransformContext, Plugin } from "vite";
|
|
23
|
+
|
|
24
|
+
export default function placementInjector(): Plugin {
|
|
25
|
+
const placementCss = `
|
|
26
|
+
<style>
|
|
27
|
+
.placement-switcher {
|
|
28
|
+
margin: 20px 0;
|
|
29
|
+
padding: 12px 15px;
|
|
30
|
+
background-color: #f0f8ff;
|
|
31
|
+
border: 2px solid #2196F3;
|
|
32
|
+
border-radius: 5px;
|
|
33
|
+
font-family: monospace;
|
|
34
|
+
font-size: 14px;
|
|
35
|
+
}
|
|
36
|
+
.placement-switcher-title {
|
|
37
|
+
font-weight: bold;
|
|
38
|
+
margin-bottom: 10px;
|
|
39
|
+
border-bottom: 1px solid #2196F3;
|
|
40
|
+
padding-bottom: 5px;
|
|
41
|
+
}
|
|
42
|
+
.placement-option {
|
|
43
|
+
display: inline-block;
|
|
44
|
+
text-decoration: none;
|
|
45
|
+
margin-right: 8px;
|
|
46
|
+
padding: 6px 14px;
|
|
47
|
+
border: 1px solid #2196F3;
|
|
48
|
+
border-radius: 4px;
|
|
49
|
+
background-color: white;
|
|
50
|
+
color: #2196F3;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
font-family: inherit;
|
|
53
|
+
font-size: inherit;
|
|
54
|
+
}
|
|
55
|
+
.placement-option[aria-current="page"] {
|
|
56
|
+
background-color: #2196F3;
|
|
57
|
+
color: white;
|
|
58
|
+
font-weight: bold;
|
|
59
|
+
}
|
|
60
|
+
.placement-note {
|
|
61
|
+
margin-top: 10px;
|
|
62
|
+
color: #555;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const placementHtml = `
|
|
68
|
+
<!-- Challenge placement switcher -->
|
|
69
|
+
<div id="placement-switcher" class="placement-switcher">
|
|
70
|
+
<div class="placement-switcher-title">Challenge placement</div>
|
|
71
|
+
<a class="placement-option" data-placement-option="popup" href="?placement=popup">popup</a>
|
|
72
|
+
<a class="placement-option" data-placement-option="float" href="?placement=float">float</a>
|
|
73
|
+
<div id="placement-note" class="placement-note"></div>
|
|
74
|
+
</div>
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
// Runs before DOMContentLoaded, so the attribute is on the container by the
|
|
78
|
+
// time the bundle's implicit render reads it.
|
|
79
|
+
const placementJs = `
|
|
80
|
+
<script>
|
|
81
|
+
(function () {
|
|
82
|
+
var requested = new URLSearchParams(window.location.search).get("placement");
|
|
83
|
+
var placement = requested === "float" ? "float" : "popup";
|
|
84
|
+
|
|
85
|
+
// Buttons are the invisible variant, which resolves float back to
|
|
86
|
+
// popup -- stamped anyway so the downgrade is visible rather than
|
|
87
|
+
// untested.
|
|
88
|
+
var containers = document.getElementsByClassName("procaptcha");
|
|
89
|
+
for (var i = 0; i < containers.length; i++) {
|
|
90
|
+
containers[i].setAttribute("data-placement", placement);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
var invisible = containers.length > 0 &&
|
|
94
|
+
document.querySelectorAll('.procaptcha[data-size="invisible"], button.procaptcha').length > 0;
|
|
95
|
+
|
|
96
|
+
var note = document.getElementById("placement-note");
|
|
97
|
+
if (note) {
|
|
98
|
+
note.textContent = placement === "float" && invisible
|
|
99
|
+
? "float requested, but an invisible widget has nothing to anchor to, so the challenge opens as a popup."
|
|
100
|
+
: placement === "float"
|
|
101
|
+
? "The challenge opens directly above the widget and stays pinned there while you scroll. The page stays usable behind it."
|
|
102
|
+
: "The challenge opens centred over the page. This is the default.";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Links rather than buttons, and not only because navigation is what
|
|
106
|
+
// they do: several specs reach for an element with
|
|
107
|
+
// button[type='button']:nth-of-type(2), which a pair of injected
|
|
108
|
+
// buttons answers to, and following one mid-test reloads the page.
|
|
109
|
+
var options = document.querySelectorAll("[data-placement-option]");
|
|
110
|
+
for (var j = 0; j < options.length; j++) {
|
|
111
|
+
var option = options[j];
|
|
112
|
+
var value = option.getAttribute("data-placement-option");
|
|
113
|
+
// Rebuilt from the live URL so the switch keeps any other query
|
|
114
|
+
// parameters the page was opened with.
|
|
115
|
+
var url = new URL(window.location.href);
|
|
116
|
+
url.searchParams.set("placement", value);
|
|
117
|
+
option.setAttribute("href", url.toString());
|
|
118
|
+
if (value === placement) option.setAttribute("aria-current", "page");
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
</script>
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
name: "placement-injector",
|
|
126
|
+
transformIndexHtml: {
|
|
127
|
+
order: "post", // after the nav bar and status log are in place
|
|
128
|
+
handler(html: string, _ctx: IndexHtmlTransformContext): string {
|
|
129
|
+
if (!html.includes("<body") || !html.includes("</body>")) {
|
|
130
|
+
return html;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (html.includes('id="placement-switcher"')) {
|
|
134
|
+
return html;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const withCss = html.replace("</head>", `${placementCss}</head>`);
|
|
138
|
+
|
|
139
|
+
// The switcher goes above the status log so the widget and its
|
|
140
|
+
// controls stay together, and the script goes last so every
|
|
141
|
+
// element it touches has been parsed.
|
|
142
|
+
const withSwitcher = withCss.includes('<div id="captcha-status"')
|
|
143
|
+
? withCss.replace(
|
|
144
|
+
'<div id="captcha-status"',
|
|
145
|
+
`${placementHtml}<div id="captcha-status"`,
|
|
146
|
+
)
|
|
147
|
+
: withCss.replace("</body>", `${placementHtml}</body>`);
|
|
148
|
+
|
|
149
|
+
return withSwitcher.replace("</body>", `${placementJs}</body>`);
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|