@prosopo/client-bundle-example 2.10.23 → 2.11.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.
- package/.turbo/turbo-build$colon$cjs.log +5 -4
- package/.turbo/turbo-build$colon$tsc.log +11 -11
- package/.turbo/turbo-build.log +6 -5
- package/CHANGELOG.md +21 -0
- package/dist/cjs/index.cjs +13 -41
- package/dist/cjs/plugins/pages.cjs +113 -0
- package/dist/index.js +2 -42
- package/dist/plugins/pages.js +103 -0
- package/package.json +7 -7
- package/src/frictionless-explicit-web3.html +10 -23
- package/src/frictionless-explicit.html +18 -22
- package/src/frictionless-implicit.html +60 -86
- package/src/frictionless-manual-start.html +13 -33
- package/src/image-explicit-web3.html +8 -24
- package/src/image-explicit.html +16 -23
- package/src/image-implicit.html +192 -0
- package/src/index.html +11 -26
- package/src/index.ts +1 -62
- package/src/invisible-frictionless-explicit.html +8 -17
- package/src/invisible-frictionless-implicit.html +8 -25
- package/src/invisible-image-explicit.html +8 -23
- package/src/invisible-image-implicit.html +8 -24
- package/src/invisible-pow-explicit.html +8 -23
- package/src/invisible-pow-implicit.html +9 -21
- package/src/invisible-puzzle-explicit.html +8 -23
- package/src/invisible-puzzle-implicit.html +9 -21
- package/src/plugins/code-snippet-injector.ts +179 -0
- package/src/plugins/form-filler-injector.ts +36 -121
- package/src/plugins/layout-injector.ts +238 -0
- package/src/plugins/logo.ts +15 -0
- package/src/plugins/pages.ts +219 -0
- package/src/plugins/placement-injector.ts +114 -0
- package/src/plugins/slots.ts +32 -0
- package/src/plugins/status-log-injector.ts +35 -114
- package/src/pow-explicit-web3.html +8 -24
- package/src/pow-explicit.html +8 -23
- package/src/pow-implicit-sessionid.html +10 -24
- package/src/pow-implicit.html +9 -24
- package/src/puzzle-bind-explicit.html +83 -0
- package/src/puzzle-explicit.html +16 -23
- package/src/puzzle-implicit.html +9 -24
- package/src/styles/demo.css +803 -0
- package/src/tests/plugins.unit.test.ts +285 -118
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.config.ts +11 -5
- package/src/plugins/explanation-injector.ts +0 -294
- package/src/plugins/navigation-injector.ts +0 -605
- package/src/styles/captcha.css +0 -78
- package/src/styles/field.css +0 -12
- package/src/tests/floatLabel.unit.test.ts +0 -195
- package/src/tests/floatLabelReady.unit.test.ts +0 -36
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
// Wraps every demo page in the playground shell: the prosopo.io header, a
|
|
16
|
+
// sidebar that links between the demo pages, and a panel for the event log,
|
|
17
|
+
// the code for the current setup and the sign-up call to action.
|
|
18
|
+
import type { IndexHtmlTransformContext, Plugin } from "vite";
|
|
19
|
+
import { prosopoWordmark } from "./logo.js";
|
|
20
|
+
import {
|
|
21
|
+
type DemoPage,
|
|
22
|
+
type DemoSetup,
|
|
23
|
+
captchaTypeOptions,
|
|
24
|
+
defaultSetup,
|
|
25
|
+
findPageByPath,
|
|
26
|
+
labelOf,
|
|
27
|
+
modeOptions,
|
|
28
|
+
pagePathFromFilename,
|
|
29
|
+
relativePrefix,
|
|
30
|
+
renderingOptions,
|
|
31
|
+
renderingsFor,
|
|
32
|
+
resolvePage,
|
|
33
|
+
} from "./pages.js";
|
|
34
|
+
import { slotMarkup } from "./slots.js";
|
|
35
|
+
|
|
36
|
+
export const SIGN_UP_URL = "https://prosopo.io/pricing/";
|
|
37
|
+
const LOGIN_URL = "https://portal.prosopo.io/";
|
|
38
|
+
const DOCS_URL = "https://docs.prosopo.io/";
|
|
39
|
+
|
|
40
|
+
const ariaCurrent = (isCurrent: boolean): string =>
|
|
41
|
+
isCurrent ? ' aria-current="page"' : "";
|
|
42
|
+
|
|
43
|
+
const headTags = (prefix: string): string => `
|
|
44
|
+
<meta charset="utf-8">
|
|
45
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
46
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
47
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
48
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700;800&display=swap">
|
|
49
|
+
<link rel="stylesheet" href="${prefix}styles/demo.css">`;
|
|
50
|
+
|
|
51
|
+
const header = `
|
|
52
|
+
<header class="demo-header">
|
|
53
|
+
<a class="demo-header__logo" href="https://prosopo.io/">${prosopoWordmark}</a>
|
|
54
|
+
<nav class="demo-header__nav" aria-label="prosopo.io">
|
|
55
|
+
<a href="https://prosopo.io/products/">Platform</a>
|
|
56
|
+
<a href="${SIGN_UP_URL}">Pricing</a>
|
|
57
|
+
<a href="${DOCS_URL}">Docs</a>
|
|
58
|
+
<a href="https://prosopo.io/blog/">Blog</a>
|
|
59
|
+
</nav>
|
|
60
|
+
<div class="demo-header__actions">
|
|
61
|
+
<a class="demo-link-button demo-link-button--muted" href="${LOGIN_URL}">Login</a>
|
|
62
|
+
<a class="demo-link-button demo-link-button--primary" href="${SIGN_UP_URL}">Get started →</a>
|
|
63
|
+
</div>
|
|
64
|
+
</header>`;
|
|
65
|
+
|
|
66
|
+
const sidebar = (page: DemoPage | undefined, prefix: string): string => {
|
|
67
|
+
const setup: DemoSetup = page ?? defaultSetup;
|
|
68
|
+
const href = (target: DemoSetup): string =>
|
|
69
|
+
`${prefix}${resolvePage(target).path}`;
|
|
70
|
+
|
|
71
|
+
const choice = (
|
|
72
|
+
target: DemoSetup,
|
|
73
|
+
isCurrent: boolean,
|
|
74
|
+
name: string,
|
|
75
|
+
description: string,
|
|
76
|
+
): string => `
|
|
77
|
+
<a class="demo-choice" data-demo-nav href="${href(target)}"${ariaCurrent(isCurrent)}>
|
|
78
|
+
<span class="demo-choice__radio"></span>
|
|
79
|
+
<span class="demo-choice__body">
|
|
80
|
+
<span class="demo-choice__name">${name}</span>
|
|
81
|
+
<span class="demo-choice__description">${description}</span>
|
|
82
|
+
</span>
|
|
83
|
+
</a>`;
|
|
84
|
+
|
|
85
|
+
const types = captchaTypeOptions
|
|
86
|
+
.map(({ value, label, description }) =>
|
|
87
|
+
choice(
|
|
88
|
+
{ ...setup, captchaType: value },
|
|
89
|
+
page?.captchaType === value,
|
|
90
|
+
value === defaultSetup.captchaType
|
|
91
|
+
? `${label}<span class="demo-badge">Default</span>`
|
|
92
|
+
: label,
|
|
93
|
+
description,
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
.join("");
|
|
97
|
+
|
|
98
|
+
const modes = modeOptions
|
|
99
|
+
.map(
|
|
100
|
+
({ value, label }) =>
|
|
101
|
+
`<a data-demo-nav href="${href({ ...setup, mode: value })}"${ariaCurrent(page?.mode === value)}>${label}</a>`,
|
|
102
|
+
)
|
|
103
|
+
.join("");
|
|
104
|
+
|
|
105
|
+
const renderings = renderingsFor(setup.captchaType, setup.mode)
|
|
106
|
+
.map(({ value, label, description }) =>
|
|
107
|
+
choice(
|
|
108
|
+
{ ...setup, rendering: value },
|
|
109
|
+
page?.rendering === value,
|
|
110
|
+
label,
|
|
111
|
+
description,
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
.join("");
|
|
115
|
+
|
|
116
|
+
// Rendering is an integration detail, so it stays folded away unless the
|
|
117
|
+
// visitor is already on a non-default rendering.
|
|
118
|
+
const advancedOpen =
|
|
119
|
+
page && page.rendering !== defaultSetup.rendering ? " open" : "";
|
|
120
|
+
|
|
121
|
+
return `
|
|
122
|
+
<aside class="demo-sidebar">
|
|
123
|
+
<div class="demo-intro-block">
|
|
124
|
+
<div class="demo-eyebrow">Live demo</div>
|
|
125
|
+
<h1 class="demo-title">Procaptcha playground</h1>
|
|
126
|
+
<p class="demo-intro">Pick a setup, fill in the form, see what happens.</p>
|
|
127
|
+
</div>
|
|
128
|
+
<nav class="demo-group" aria-label="Captcha type">
|
|
129
|
+
<div class="demo-group__label">Captcha type</div>
|
|
130
|
+
<div class="demo-types">${types}
|
|
131
|
+
</div>
|
|
132
|
+
</nav>
|
|
133
|
+
<div class="demo-group">
|
|
134
|
+
<div class="demo-group__label">Mode</div>
|
|
135
|
+
<nav class="demo-segmented" aria-label="Mode">${modes}</nav>
|
|
136
|
+
</div>
|
|
137
|
+
${slotMarkup("placement")}
|
|
138
|
+
<details class="demo-advanced"${advancedOpen}>
|
|
139
|
+
<summary>Advanced</summary>
|
|
140
|
+
<div class="demo-group">
|
|
141
|
+
<div class="demo-group__label">Rendering</div>
|
|
142
|
+
<p class="demo-group__hint">How the widget gets onto your page. Most sites use implicit.</p>
|
|
143
|
+
<nav class="demo-renderings" aria-label="Rendering">${renderings}
|
|
144
|
+
</nav>
|
|
145
|
+
</div>
|
|
146
|
+
</details>
|
|
147
|
+
</aside>`;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const chips = (page: DemoPage | undefined): string =>
|
|
151
|
+
page
|
|
152
|
+
? [
|
|
153
|
+
labelOf(captchaTypeOptions, page.captchaType),
|
|
154
|
+
labelOf(modeOptions, page.mode),
|
|
155
|
+
...(page.rendering === defaultSetup.rendering
|
|
156
|
+
? []
|
|
157
|
+
: [labelOf(renderingOptions, page.rendering)]),
|
|
158
|
+
]
|
|
159
|
+
.map((label) => `<span class="demo-chip">${label}</span>`)
|
|
160
|
+
.join("")
|
|
161
|
+
: "";
|
|
162
|
+
|
|
163
|
+
const layoutOpen = (page: DemoPage | undefined, prefix: string): string => `
|
|
164
|
+
<div class="demo-layout">
|
|
165
|
+
${sidebar(page, prefix)}
|
|
166
|
+
<main class="demo-main">
|
|
167
|
+
<div class="demo-toolbar">${chips(page)}${slotMarkup("toolbar")}</div>
|
|
168
|
+
`;
|
|
169
|
+
|
|
170
|
+
// Pages outside the registry (web3, session id) have no snippet to show.
|
|
171
|
+
const codeSection = (page: DemoPage | undefined): string =>
|
|
172
|
+
page
|
|
173
|
+
? `
|
|
174
|
+
<section class="demo-panel__section">
|
|
175
|
+
<h2 class="demo-panel__title">Code for this setup</h2>
|
|
176
|
+
${slotMarkup("code")}
|
|
177
|
+
</section>`
|
|
178
|
+
: "";
|
|
179
|
+
|
|
180
|
+
const layoutClose = (page: DemoPage | undefined): string => `
|
|
181
|
+
<p class="demo-cta-line">Want this on your site? <a href="${SIGN_UP_URL}">Start free, no credit card →</a></p>
|
|
182
|
+
</main>
|
|
183
|
+
<aside class="demo-panel" aria-label="What happened">
|
|
184
|
+
<section class="demo-panel__section">
|
|
185
|
+
<h2 class="demo-panel__title">Events</h2>
|
|
186
|
+
${slotMarkup("events")}
|
|
187
|
+
</section>${codeSection(page)}
|
|
188
|
+
<div class="demo-panel__cta">
|
|
189
|
+
<h2>Get your own site key</h2>
|
|
190
|
+
<p>Free plan. No credit card required.</p>
|
|
191
|
+
<a class="demo-link-button demo-link-button--pink" href="${SIGN_UP_URL}">Get started →</a>
|
|
192
|
+
</div>
|
|
193
|
+
</aside>
|
|
194
|
+
</div>`;
|
|
195
|
+
|
|
196
|
+
// Placement is a query parameter, so carry it across when moving between pages.
|
|
197
|
+
const keepPlacementScript = `
|
|
198
|
+
<script>
|
|
199
|
+
(function () {
|
|
200
|
+
var placement = new URLSearchParams(window.location.search).get("placement");
|
|
201
|
+
if (!placement) return;
|
|
202
|
+
var links = document.querySelectorAll("a[data-demo-nav]");
|
|
203
|
+
for (var i = 0; i < links.length; i++) {
|
|
204
|
+
var url = new URL(links[i].getAttribute("href"), window.location.href);
|
|
205
|
+
url.searchParams.set("placement", placement);
|
|
206
|
+
links[i].setAttribute("href", url.toString());
|
|
207
|
+
}
|
|
208
|
+
})();
|
|
209
|
+
</script>`;
|
|
210
|
+
|
|
211
|
+
export default function layoutInjector(): Plugin {
|
|
212
|
+
return {
|
|
213
|
+
name: "layout-injector",
|
|
214
|
+
transformIndexHtml: {
|
|
215
|
+
order: "pre",
|
|
216
|
+
handler(html: string, ctx: IndexHtmlTransformContext): string {
|
|
217
|
+
if (!html.includes("<body>") || !html.includes("</body>")) {
|
|
218
|
+
return html;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const pagePath = pagePathFromFilename(ctx.filename);
|
|
222
|
+
const page = findPageByPath(pagePath);
|
|
223
|
+
const prefix = relativePrefix(pagePath);
|
|
224
|
+
|
|
225
|
+
return html
|
|
226
|
+
.replace("<head>", () => `<head>${headTags(prefix)}`)
|
|
227
|
+
.replace(
|
|
228
|
+
"<body>",
|
|
229
|
+
() => `<body class="demo">${header}${layoutOpen(page, prefix)}`,
|
|
230
|
+
)
|
|
231
|
+
.replace(
|
|
232
|
+
"</body>",
|
|
233
|
+
() => `${layoutClose(page)}${keepPlacementScript}\n</body>`,
|
|
234
|
+
);
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
export const prosopoWordmark = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2062.63 468.67" role="img" aria-label="Prosopo"><g fill="currentColor" transform="translate(-215.73 -1774.69)"><path d="M335.55,1825.19A147.75,147.75,0,0,1,483.3,1972.94h50.5c0-109.49-88.76-198.25-198.25-198.25v50.5Z"/><path d="M269.36,1891.39A147.74,147.74,0,0,1,417.1,2039.13h50.5c0-109.49-88.75-198.24-198.24-198.24v50.5Z"/><path d="M414,2157.17a147.75,147.75,0,0,1-147.74-147.74h-50.5c0,109.49,88.75,198.24,198.24,198.24v-50.5Z"/><path d="M480.17,2091a147.74,147.74,0,0,1-147.74-147.75H281.92c0,109.49,88.76,198.25,198.25,198.25V2091Z"/><path d="M862.8,2017.5q-27.39,22.86-78.25,22.86h-65v112.19H654.82v-312h134q46.32,0,73.86,24.13t27.55,74.72Q890.2,1994.64,862.8,2017.5ZM813,1905.1q-12.37-10.36-34.7-10.38H719.59v91.87h58.75q22.32,0,34.7-11.22t12.39-35.56Q825.43,1915.48,813,1905.1Z"/><path d="M1045.69,1916.42c.78.08,2.51.19,5.19.32v61.81c-3.81-.42-7.2-.71-10.16-.85s-5.36-.21-7.2-.21q-36.4,0-48.89,23.71-7,13.33-7,41.06v110.29H916.89V1921.82h57.58V1962q14-23.07,24.34-31.54,16.94-14.18,44-14.18C1044,1916.32,1044.92,1916.35,1045.69,1916.42Z"/><path d="M1265.64,2124.32q-29.21,36.06-88.69,36.06t-88.69-36.06Q1059,2088.26,1059,2037.5q0-49.9,29.22-86.5t88.69-36.59q59.47,0,88.69,36.59t29.21,86.5Q1294.85,2088.26,1265.64,2124.32ZM1217.38,2091q14.17-18.81,14.18-53.48t-14.18-53.37q-14.19-18.7-40.64-18.71T1136,1984.13q-14.29,18.72-14.29,53.37T1136,2091q14.28,18.81,40.75,18.81T1217.38,2091Z"/><path d="M1371.81,2078.88q1.92,16.1,8.29,22.87,11.28,12.06,41.7,12.06,17.85,0,28.39-5.29t10.53-15.88a17.12,17.12,0,0,0-8.48-15.45q-8.49-5.28-63.12-18.2-39.33-9.73-55.41-24.35-16.08-14.39-16.09-41.49,0-32,25.14-54.93t70.75-23q43.26,0,70.53,17.25t31.29,59.59H1455q-1.27-11.64-6.58-18.42-10-12.27-34-12.28-19.74,0-28.13,6.14t-8.38,14.4c0,6.91,3,11.93,8.92,15q8.89,4.89,63,16.73,36,8.46,54.05,25.61,17.77,17.35,17.78,43.39,0,34.3-25.56,56t-79,21.7q-54.51,0-80.49-23t-26-58.53Z"/><path d="M1745.54,2124.32q-29.22,36.06-88.7,36.06t-88.69-36.06q-29.2-36.06-29.21-86.82,0-49.9,29.21-86.5t88.69-36.59q59.49,0,88.7,36.59t29.21,86.5Q1774.75,2088.26,1745.54,2124.32ZM1697.27,2091q14.19-18.81,14.19-53.48t-14.19-53.37q-14.18-18.7-40.64-18.71t-40.75,18.71q-14.28,18.72-14.28,53.37t14.28,53.48q14.3,18.81,40.75,18.81T1697.27,2091Z"/><path d="M1992.75,1946.59q28.24,29.84,28.23,87.63,0,61-27.58,92.93t-71.06,32q-27.69,0-46-13.76-10-7.62-19.6-22.23v120.24H1797V1921.82h57.79v34.08q9.79-15,20.88-23.71,20.23-15.43,48.15-15.45Q1964.53,1916.74,1992.75,1946.59Zm-46.3,43.39q-12.3-20.52-39.88-20.53-33.15,0-45.54,31.11-6.43,16.51-6.42,41.92,0,40.21,21.58,56.51,12.82,9.53,30.37,9.53,25.45,0,38.83-19.48t13.36-51.86Q1958.75,2010.51,1946.45,1990Z"/><path d="M2249.14,2124.32q-29.2,36.06-88.69,36.06t-88.69-36.06q-29.22-36.06-29.21-86.82,0-49.9,29.21-86.5t88.69-36.59q59.49,0,88.69,36.59t29.22,86.5Q2278.36,2088.26,2249.14,2124.32ZM2200.88,2091q14.19-18.81,14.18-53.48t-14.18-53.37q-14.18-18.7-40.64-18.71t-40.75,18.71q-14.28,18.72-14.29,53.37t14.29,53.48q14.3,18.81,40.75,18.81T2200.88,2091Z"/></g></svg>`;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import { CaptchaType } from "@prosopo/types";
|
|
16
|
+
|
|
17
|
+
export type DemoCaptchaType =
|
|
18
|
+
| CaptchaType.frictionless
|
|
19
|
+
| CaptchaType.pow
|
|
20
|
+
| CaptchaType.puzzle
|
|
21
|
+
| CaptchaType.image;
|
|
22
|
+
|
|
23
|
+
export type DemoMode = "standard" | "invisible";
|
|
24
|
+
|
|
25
|
+
export type DemoRendering = "implicit" | "explicit" | "manual" | "bound";
|
|
26
|
+
|
|
27
|
+
export interface DemoSetup {
|
|
28
|
+
captchaType: DemoCaptchaType;
|
|
29
|
+
mode: DemoMode;
|
|
30
|
+
rendering: DemoRendering;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DemoPage extends DemoSetup {
|
|
34
|
+
path: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DemoOption<T> {
|
|
38
|
+
value: T;
|
|
39
|
+
label: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CaptchaTypeOption extends DemoOption<DemoCaptchaType> {
|
|
43
|
+
description: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const captchaTypeOptions: CaptchaTypeOption[] = [
|
|
47
|
+
{
|
|
48
|
+
value: CaptchaType.frictionless,
|
|
49
|
+
label: "Frictionless",
|
|
50
|
+
description: "Humans pass without a challenge. Bots get one.",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
value: CaptchaType.pow,
|
|
54
|
+
label: "Proof of Work",
|
|
55
|
+
description: "The browser does a quick computation.",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
value: CaptchaType.puzzle,
|
|
59
|
+
label: "Puzzle",
|
|
60
|
+
description: "Drag a piece into place.",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
value: CaptchaType.image,
|
|
64
|
+
label: "Image",
|
|
65
|
+
description: "Pick the images that match.",
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
export const modeOptions: DemoOption<DemoMode>[] = [
|
|
70
|
+
{ value: "standard", label: "Standard" },
|
|
71
|
+
{ value: "invisible", label: "Invisible" },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
export interface RenderingOption extends DemoOption<DemoRendering> {
|
|
75
|
+
description: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const renderingOptions: RenderingOption[] = [
|
|
79
|
+
{
|
|
80
|
+
value: "implicit",
|
|
81
|
+
label: "Implicit",
|
|
82
|
+
description:
|
|
83
|
+
"Add a div with your site key and the script renders the widget.",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
value: "explicit",
|
|
87
|
+
label: "Explicit",
|
|
88
|
+
description: "Your own code calls render() when it is ready.",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
value: "manual",
|
|
92
|
+
label: "Manual start",
|
|
93
|
+
description:
|
|
94
|
+
"The widget shows straight away, but checks only start when you say.",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
value: "bound",
|
|
98
|
+
label: "Bound button",
|
|
99
|
+
description: "Your own submit button opens the challenge.",
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const page = (
|
|
104
|
+
path: string,
|
|
105
|
+
captchaType: DemoCaptchaType,
|
|
106
|
+
mode: DemoMode,
|
|
107
|
+
rendering: DemoRendering,
|
|
108
|
+
): DemoPage => ({ path, captchaType, mode, rendering });
|
|
109
|
+
|
|
110
|
+
const indexPage = page(
|
|
111
|
+
"index.html",
|
|
112
|
+
CaptchaType.frictionless,
|
|
113
|
+
"standard",
|
|
114
|
+
"implicit",
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
export const defaultSetup: DemoSetup = indexPage;
|
|
118
|
+
|
|
119
|
+
export const demoPages: DemoPage[] = [
|
|
120
|
+
indexPage,
|
|
121
|
+
page(
|
|
122
|
+
"frictionless-explicit.html",
|
|
123
|
+
CaptchaType.frictionless,
|
|
124
|
+
"standard",
|
|
125
|
+
"explicit",
|
|
126
|
+
),
|
|
127
|
+
page(
|
|
128
|
+
"frictionless-manual-start.html",
|
|
129
|
+
CaptchaType.frictionless,
|
|
130
|
+
"standard",
|
|
131
|
+
"manual",
|
|
132
|
+
),
|
|
133
|
+
page(
|
|
134
|
+
"invisible-frictionless-implicit.html",
|
|
135
|
+
CaptchaType.frictionless,
|
|
136
|
+
"invisible",
|
|
137
|
+
"implicit",
|
|
138
|
+
),
|
|
139
|
+
page(
|
|
140
|
+
"invisible-frictionless-explicit.html",
|
|
141
|
+
CaptchaType.frictionless,
|
|
142
|
+
"invisible",
|
|
143
|
+
"explicit",
|
|
144
|
+
),
|
|
145
|
+
page("pow-implicit.html", CaptchaType.pow, "standard", "implicit"),
|
|
146
|
+
page("pow-explicit.html", CaptchaType.pow, "standard", "explicit"),
|
|
147
|
+
page("invisible-pow-implicit.html", CaptchaType.pow, "invisible", "implicit"),
|
|
148
|
+
page("invisible-pow-explicit.html", CaptchaType.pow, "invisible", "explicit"),
|
|
149
|
+
page("puzzle-implicit.html", CaptchaType.puzzle, "standard", "implicit"),
|
|
150
|
+
page("puzzle-explicit.html", CaptchaType.puzzle, "standard", "explicit"),
|
|
151
|
+
page("puzzle-bind-explicit.html", CaptchaType.puzzle, "standard", "bound"),
|
|
152
|
+
page(
|
|
153
|
+
"invisible-puzzle-implicit.html",
|
|
154
|
+
CaptchaType.puzzle,
|
|
155
|
+
"invisible",
|
|
156
|
+
"implicit",
|
|
157
|
+
),
|
|
158
|
+
page(
|
|
159
|
+
"invisible-puzzle-explicit.html",
|
|
160
|
+
CaptchaType.puzzle,
|
|
161
|
+
"invisible",
|
|
162
|
+
"explicit",
|
|
163
|
+
),
|
|
164
|
+
page("image-implicit.html", CaptchaType.image, "standard", "implicit"),
|
|
165
|
+
page("image-explicit.html", CaptchaType.image, "standard", "explicit"),
|
|
166
|
+
page(
|
|
167
|
+
"invisible-image-implicit.html",
|
|
168
|
+
CaptchaType.image,
|
|
169
|
+
"invisible",
|
|
170
|
+
"implicit",
|
|
171
|
+
),
|
|
172
|
+
page(
|
|
173
|
+
"invisible-image-explicit.html",
|
|
174
|
+
CaptchaType.image,
|
|
175
|
+
"invisible",
|
|
176
|
+
"explicit",
|
|
177
|
+
),
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
const isSetup = (candidate: DemoSetup, setup: DemoSetup): boolean =>
|
|
181
|
+
candidate.captchaType === setup.captchaType &&
|
|
182
|
+
candidate.mode === setup.mode &&
|
|
183
|
+
candidate.rendering === setup.rendering;
|
|
184
|
+
|
|
185
|
+
export const findPageByPath = (path: string): DemoPage | undefined =>
|
|
186
|
+
demoPages.find((demoPage) => demoPage.path === path);
|
|
187
|
+
|
|
188
|
+
// Not every combination has a page (there is no invisible manual start, for
|
|
189
|
+
// example), so fall back to implicit rendering, then to the standard mode.
|
|
190
|
+
export const resolvePage = (setup: DemoSetup): DemoPage =>
|
|
191
|
+
demoPages.find((demoPage) => isSetup(demoPage, setup)) ??
|
|
192
|
+
demoPages.find((demoPage) =>
|
|
193
|
+
isSetup(demoPage, { ...setup, rendering: "implicit" }),
|
|
194
|
+
) ??
|
|
195
|
+
demoPages.find((demoPage) =>
|
|
196
|
+
isSetup(demoPage, { ...setup, mode: "standard", rendering: "implicit" }),
|
|
197
|
+
) ??
|
|
198
|
+
indexPage;
|
|
199
|
+
|
|
200
|
+
export const renderingsFor = (
|
|
201
|
+
captchaType: DemoCaptchaType,
|
|
202
|
+
mode: DemoMode,
|
|
203
|
+
): RenderingOption[] =>
|
|
204
|
+
renderingOptions.filter(({ value }) =>
|
|
205
|
+
demoPages.some((demoPage) =>
|
|
206
|
+
isSetup(demoPage, { captchaType, mode, rendering: value }),
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
export const labelOf = <T>(options: DemoOption<T>[], value: T): string =>
|
|
211
|
+
options.find((option) => option.value === value)?.label ?? String(value);
|
|
212
|
+
|
|
213
|
+
export const pagePathFromFilename = (filename: string | undefined): string => {
|
|
214
|
+
const normalized = (filename ?? "").replace(/\\/g, "/");
|
|
215
|
+
return normalized.match(/^(?:.*\/)?(?:src|dist)\/(.+)$/)?.[1] ?? "";
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export const relativePrefix = (pagePath: string): string =>
|
|
219
|
+
"../".repeat((pagePath.match(/\//g) ?? []).length);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
// Vite plugin to inject a popup/float placement switcher into HTML files.
|
|
16
|
+
//
|
|
17
|
+
// Placement is resolved once, when the widget renders, so switching it means
|
|
18
|
+
// re-rendering. The switcher therefore drives `?placement=` and reloads rather
|
|
19
|
+
// than mutating a live widget. The explicit pages read that query parameter
|
|
20
|
+
// themselves and pass `placement` as a render option; implicit pages have no
|
|
21
|
+
// script of their own, so the injected script stamps `data-placement` onto
|
|
22
|
+
// their containers instead, which exercises the attribute path.
|
|
23
|
+
import type { IndexHtmlTransformContext, Plugin } from "vite";
|
|
24
|
+
import { fillSlot } from "./slots.js";
|
|
25
|
+
|
|
26
|
+
export default function placementInjector(): Plugin {
|
|
27
|
+
const placementHtml = `
|
|
28
|
+
<div id="placement-switcher" class="demo-group">
|
|
29
|
+
<div class="demo-group__label">Challenge placement</div>
|
|
30
|
+
<nav class="demo-segmented" aria-label="Challenge placement">
|
|
31
|
+
<a data-placement-option="popup" href="?placement=popup">Popup</a>
|
|
32
|
+
<a data-placement-option="float" href="?placement=float">Float</a>
|
|
33
|
+
</nav>
|
|
34
|
+
<p id="placement-note" class="demo-placement-note"></p>
|
|
35
|
+
</div>
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
// Runs before DOMContentLoaded, so the attribute is on the container by the
|
|
39
|
+
// time the bundle's implicit render reads it.
|
|
40
|
+
const placementJs = `
|
|
41
|
+
<script>
|
|
42
|
+
(function () {
|
|
43
|
+
var requested = new URLSearchParams(window.location.search).get("placement");
|
|
44
|
+
var placement = requested === "float" ? "float" : "popup";
|
|
45
|
+
|
|
46
|
+
// Buttons are the invisible variant, which resolves float back to
|
|
47
|
+
// popup -- stamped anyway so the downgrade is visible rather than
|
|
48
|
+
// untested.
|
|
49
|
+
var containers = document.getElementsByClassName("procaptcha");
|
|
50
|
+
for (var i = 0; i < containers.length; i++) {
|
|
51
|
+
containers[i].setAttribute("data-placement", placement);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
var invisible = containers.length > 0 &&
|
|
55
|
+
document.querySelectorAll('.procaptcha[data-size="invisible"], button.procaptcha').length > 0;
|
|
56
|
+
|
|
57
|
+
var note = document.getElementById("placement-note");
|
|
58
|
+
if (note) {
|
|
59
|
+
note.textContent = placement === "float" && invisible
|
|
60
|
+
? "An invisible widget has nothing to anchor to, so the challenge opens as a popup."
|
|
61
|
+
: placement === "float"
|
|
62
|
+
? "The challenge opens next to the widget and stays pinned while you scroll."
|
|
63
|
+
: "The challenge opens in the middle of the page.";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Links rather than buttons, and not only because navigation is what
|
|
67
|
+
// they do: several specs reach for an element with
|
|
68
|
+
// button[type='button']:nth-of-type(2), which a pair of injected
|
|
69
|
+
// buttons answers to, and following one mid-test reloads the page.
|
|
70
|
+
var options = document.querySelectorAll("[data-placement-option]");
|
|
71
|
+
for (var j = 0; j < options.length; j++) {
|
|
72
|
+
var option = options[j];
|
|
73
|
+
var value = option.getAttribute("data-placement-option");
|
|
74
|
+
// Rebuilt from the live URL so the switch keeps any other query
|
|
75
|
+
// parameters the page was opened with.
|
|
76
|
+
var url = new URL(window.location.href);
|
|
77
|
+
url.searchParams.set("placement", value);
|
|
78
|
+
option.setAttribute("href", url.toString());
|
|
79
|
+
if (value === placement) option.setAttribute("aria-current", "page");
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
</script>
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
name: "placement-injector",
|
|
87
|
+
transformIndexHtml: {
|
|
88
|
+
order: "post",
|
|
89
|
+
handler(html: string, _ctx: IndexHtmlTransformContext): string {
|
|
90
|
+
if (!html.includes("<body") || !html.includes("</body>")) {
|
|
91
|
+
return html;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (html.includes('id="placement-switcher"')) {
|
|
95
|
+
return html;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Without the layout, the switcher goes above the status log so the
|
|
99
|
+
// widget and its controls stay together.
|
|
100
|
+
const withSwitcher =
|
|
101
|
+
fillSlot(html, "placement", placementHtml) ??
|
|
102
|
+
(html.includes('<div id="captcha-status"')
|
|
103
|
+
? html.replace(
|
|
104
|
+
'<div id="captcha-status"',
|
|
105
|
+
() => `${placementHtml}<div id="captcha-status"`,
|
|
106
|
+
)
|
|
107
|
+
: html.replace("</body>", () => `${placementHtml}</body>`));
|
|
108
|
+
|
|
109
|
+
// The script goes last so every element it touches has been parsed.
|
|
110
|
+
return withSwitcher.replace("</body>", () => `${placementJs}</body>`);
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
// The layout plugin leaves empty placeholders that the other plugins fill, so
|
|
16
|
+
// each plugin still works on its own when the layout is not there.
|
|
17
|
+
|
|
18
|
+
export type DemoSlot = "placement" | "toolbar" | "events" | "code";
|
|
19
|
+
|
|
20
|
+
export const slotMarkup = (slot: DemoSlot): string =>
|
|
21
|
+
`<div data-demo-slot="${slot}"></div>`;
|
|
22
|
+
|
|
23
|
+
export const fillSlot = (
|
|
24
|
+
html: string,
|
|
25
|
+
slot: DemoSlot,
|
|
26
|
+
content: string,
|
|
27
|
+
): string | undefined => {
|
|
28
|
+
const marker = slotMarkup(slot);
|
|
29
|
+
return html.includes(marker)
|
|
30
|
+
? html.replace(marker, () => content)
|
|
31
|
+
: undefined;
|
|
32
|
+
};
|