@prosopo/procaptcha-frictionless 2.13.17 → 2.13.18
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 +24 -24
- package/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +22 -0
- package/dist/tests/escalationClientSession.test.d.ts +5 -0
- package/dist/tests/escalationClientSession.test.d.ts.map +1 -0
- package/dist/tests/escalationClientSession.test.js +132 -0
- package/dist/tests/escalationClientSession.test.js.map +1 -0
- package/package.json +10 -9
- package/src/tests/escalationClientSession.test.tsx +225 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @prosopo/procaptcha-frictionless@2.13.
|
|
2
|
+
> @prosopo/procaptcha-frictionless@2.13.18 build:cjs
|
|
3
3
|
> NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV
|
|
4
4
|
|
|
5
5
|
ViteCommonJSConfig: .
|
|
@@ -64,4 +64,4 @@ dist/cjs/detectorPrefetch.cjs 2.98 kB │ gzip: 1.35 kB
|
|
|
64
64
|
dist/cjs/customDetectBot.cjs 5.55 kB │ gzip: 1.70 kB
|
|
65
65
|
dist/cjs/ProcaptchaFrictionless.cjs 7.68 kB │ gzip: 2.17 kB
|
|
66
66
|
|
|
67
|
-
[32m✓ built in
|
|
67
|
+
[32m✓ built in 50ms[39m
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
|
-
> @prosopo/procaptcha-frictionless@2.13.
|
|
2
|
+
> @prosopo/procaptcha-frictionless@2.13.18 build:tsc
|
|
3
3
|
> tsc --build --verbose
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
1:43:30 PM - Projects in this build:
|
|
6
6
|
* ../../dev/config/tsconfig.json
|
|
7
7
|
* ../locale/tsconfig.json
|
|
8
8
|
* ../util/tsconfig.json
|
|
@@ -25,47 +25,47 @@
|
|
|
25
25
|
* ../procaptcha-react/tsconfig.json
|
|
26
26
|
* tsconfig.json
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
1:43:30 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'
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
1:43:30 PM - Project '../locale/tsconfig.json' is up to date because newest input '../locale/src/translationKey.ts' is older than output '../locale/tsconfig.tsbuildinfo'
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
1:43:30 PM - Project '../util/tsconfig.json' is up to date because newest input '../util/src/url.ts' is older than output '../util/tsconfig.tsbuildinfo'
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
1:43:30 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'
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
1:43:30 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'
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
1:43:30 PM - Project '../api/tsconfig.json' is up to date because newest input '../api/src/tests/apiHarness.ts' is older than output '../api/tsconfig.tsbuildinfo'
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
1:43:30 PM - Project '../logger/tsconfig.json' is up to date because newest input '../logger/src/index.ts' is older than output '../logger/tsconfig.tsbuildinfo'
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
1:43:31 PM - Project '../common/tsconfig.json' is up to date because newest input '../common/src/index.ts' is older than output '../common/tsconfig.tsbuildinfo'
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
1:43:31 PM - Project '../fingerprintjs/tsconfig.json' is up to date because newest input '../fingerprintjs/src/utils/browser.ts' is older than output '../fingerprintjs/tsconfig.tsbuildinfo'
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
1:43:31 PM - Project '../fingerprint/tsconfig.json' is up to date because newest input '../fingerprint/src/merkle.ts' is older than output '../fingerprint/tsconfig.tsbuildinfo'
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
1:43:31 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'
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
1:43:31 PM - Project '../widget-skeleton/tsconfig.json' is up to date because newest input '../widget-skeleton/src/theme.ts' is older than output '../widget-skeleton/tsconfig.tsbuildinfo'
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
1:43:31 PM - Project '../keyring/tsconfig.json' is up to date because newest input '../keyring/src/keyring/keyring.test.ts' is older than output '../keyring/tsconfig.tsbuildinfo'
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
1:43:31 PM - Project '../account/tsconfig.json' is up to date because newest input '../account/src/index.ts' is older than output '../account/tsconfig.tsbuildinfo'
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
1:43:31 PM - Project '../procaptcha-common/tsconfig.json' is up to date because newest input '../procaptcha-common/src/simdReadings.ts' is older than output '../procaptcha-common/tsconfig.tsbuildinfo'
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
1:43:31 PM - Project '../procaptcha-pow/tsconfig.json' is up to date because newest input '../procaptcha-pow/src/tests/managerHarness.ts' is older than output '../procaptcha-pow/tsconfig.tsbuildinfo'
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
1:43:31 PM - Project '../procaptcha-puzzle/tsconfig.json' is up to date because newest input '../procaptcha-puzzle/src/index.ts' is older than output '../procaptcha-puzzle/tsconfig.tsbuildinfo'
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
1:43:31 PM - Project '../datasets/tsconfig.json' is up to date because newest input '../datasets/src/tests/mocks/data/captchas.ts' is older than output '../datasets/tsconfig.tsbuildinfo'
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
1:43:31 PM - Project '../procaptcha/tsconfig.json' is up to date because newest input '../procaptcha/src/index.ts' is older than output '../procaptcha/tsconfig.tsbuildinfo'
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
1:43:31 PM - Project '../procaptcha-react/tsconfig.json' is up to date because newest input '../procaptcha-react/src/tests/captchaComponent.unit.test.tsx' is older than output '../procaptcha-react/tsconfig.tsbuildinfo'
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
1:43:31 PM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
1:43:31 PM - Building project '/home/runner/work/captcha/captcha/packages/procaptcha-frictionless/tsconfig.json'...
|
|
71
71
|
|
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
|
-
> @prosopo/procaptcha-frictionless@2.13.
|
|
2
|
+
> @prosopo/procaptcha-frictionless@2.13.18 build
|
|
3
3
|
> npm run build:cross-env -- --mode ${NODE_ENV:-development}
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
> @prosopo/procaptcha-frictionless@2.13.
|
|
6
|
+
> @prosopo/procaptcha-frictionless@2.13.18 build:cross-env
|
|
7
7
|
> vite build --config vite.esm.config.ts --mode production
|
|
8
8
|
|
|
9
9
|
ViteEsmConfig: .
|
|
@@ -69,4 +69,4 @@ dist/detectorPrefetch.js 2.86 kB │ gzip: 1.32 kB
|
|
|
69
69
|
dist/customDetectBot.js 5.37 kB │ gzip: 1.67 kB
|
|
70
70
|
dist/ProcaptchaFrictionless.js 7.09 kB │ gzip: 2.12 kB
|
|
71
71
|
|
|
72
|
-
[32m✓ built in
|
|
72
|
+
[32m✓ built in 59ms[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @prosopo/procaptcha-frictionless
|
|
2
2
|
|
|
3
|
+
## 2.13.18
|
|
4
|
+
### Patch Changes
|
|
5
|
+
|
|
6
|
+
- 4b1cb19: Correlate a site-supplied session id across render and verify.
|
|
7
|
+
|
|
8
|
+
A site can now hand the widget its own session identifier — Protect's JTI, or any per-user session id it already holds — and have the provider confirm at verify time that the token was earned in that same session. Render it with `data-sessionid="..."` or `renderOptions.sessionId`, resolved the same way `mode` and `language` already are, so implicit, explicit and invisible-button renders all pick it up. Pass the same value as the new trailing `clientSessionId` argument to `ProsopoServer.isVerified`.
|
|
9
|
+
|
|
10
|
+
The widget attaches it to the solution as `clientMetaData.clientSessionId`. It is persisted on the captcha record (PoW, puzzle and image alike) and mirrored to a new top-level `clientMetaData` key on the session record — an object rather than a flat field, because more render-time metadata is expected to land there. It survives the PoW→image/puzzle escalation handoff, since the escalated widget is mounted with the same config.
|
|
11
|
+
|
|
12
|
+
At verify, when the value is supplied and the solve does not carry exactly that value — including carrying none at all, which is what a token minted outside the site's session looks like — the token is disapproved with the new `ResultReason.CLIENT_SESSION_MISMATCH` (`API.CLIENT_SESSION_MISMATCH`, translated in all 31 locales), recorded on both the captcha record and the session.
|
|
13
|
+
|
|
14
|
+
Omitting the id preserves existing behaviour, so this is opt-in and backward compatible. The verify request field is `clientSessionId` rather than `sessionId` because `VerificationResponse.sessionId` already means the provider's own frictionless session; same-named request and response fields meaning different things would be a trap for integrators.
|
|
15
|
+
- Updated dependencies [4b1cb19]
|
|
16
|
+
- @prosopo/types@5.4.0
|
|
17
|
+
- @prosopo/locale@3.4.0
|
|
18
|
+
- @prosopo/procaptcha-common@2.12.0
|
|
19
|
+
- @prosopo/procaptcha-pow@2.11.0
|
|
20
|
+
- @prosopo/procaptcha-puzzle@2.11.0
|
|
21
|
+
- @prosopo/api@4.1.0
|
|
22
|
+
- @prosopo/common@3.1.52
|
|
23
|
+
- @prosopo/procaptcha-react@2.9.116
|
|
24
|
+
|
|
3
25
|
## 2.13.17
|
|
4
26
|
### Patch Changes
|
|
5
27
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escalationClientSession.test.d.ts","sourceRoot":"","sources":["../../src/tests/escalationClientSession.test.tsx"],"names":[],"mappings":"AA2BA,OAAO,CAAC,MAAM,CAAC;IACd,IAAI,wBAAwB,EAAE,OAAO,CAAC;CACtC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { CaptchaType, ModeEnum, } from "@prosopo/types";
|
|
2
|
+
import { act, createElement } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
6
|
+
const mocks = vi.hoisted(() => ({
|
|
7
|
+
mounts: [],
|
|
8
|
+
}));
|
|
9
|
+
const stub = (widget) => (props) => {
|
|
10
|
+
mocks.mounts.push({ widget, props });
|
|
11
|
+
return createElement("div", { "data-widget": widget });
|
|
12
|
+
};
|
|
13
|
+
vi.mock("@prosopo/procaptcha-pow", () => ({ ProcaptchaPow: stub("pow") }));
|
|
14
|
+
vi.mock("@prosopo/procaptcha-react", () => ({ Procaptcha: stub("image") }));
|
|
15
|
+
vi.mock("@prosopo/procaptcha-puzzle", () => ({
|
|
16
|
+
ProcaptchaPuzzle: stub("puzzle"),
|
|
17
|
+
}));
|
|
18
|
+
vi.mock("@prosopo/procaptcha-common", async (importOriginal) => {
|
|
19
|
+
const actual = await importOriginal();
|
|
20
|
+
return { ...actual, isSecureBrowserContext: () => true };
|
|
21
|
+
});
|
|
22
|
+
const { ProcaptchaFrictionless } = await import("../ProcaptchaFrictionless.js");
|
|
23
|
+
const SITE_KEY = "5siteKey";
|
|
24
|
+
const CLIENT_SESSION_ID = "jti-from-the-site";
|
|
25
|
+
const FIRST_SESSION_ID = "provider-session-1";
|
|
26
|
+
const ESCALATED_SESSION_ID = "provider-session-2";
|
|
27
|
+
const config = () => ({
|
|
28
|
+
account: { address: SITE_KEY },
|
|
29
|
+
userAccountAddress: "",
|
|
30
|
+
web2: true,
|
|
31
|
+
mode: ModeEnum.visible,
|
|
32
|
+
clientSessionId: CLIENT_SESSION_ID,
|
|
33
|
+
});
|
|
34
|
+
const i18nStub = {
|
|
35
|
+
isInitialized: true,
|
|
36
|
+
language: "en",
|
|
37
|
+
t: (key) => key,
|
|
38
|
+
changeLanguage: vi.fn(),
|
|
39
|
+
};
|
|
40
|
+
const detectBotReturning = (captchaType) => vi.fn().mockResolvedValue({
|
|
41
|
+
captchaType,
|
|
42
|
+
sessionId: FIRST_SESSION_ID,
|
|
43
|
+
provider: { provider: { url: "https://provider.test" } },
|
|
44
|
+
userAccount: "userAccount",
|
|
45
|
+
});
|
|
46
|
+
let container;
|
|
47
|
+
let root;
|
|
48
|
+
const mountWrapper = async (captchaType) => {
|
|
49
|
+
await act(async () => {
|
|
50
|
+
root.render(createElement(ProcaptchaFrictionless, {
|
|
51
|
+
config: config(),
|
|
52
|
+
callbacks: {},
|
|
53
|
+
restart: vi.fn(),
|
|
54
|
+
i18n: i18nStub,
|
|
55
|
+
detectBot: detectBotReturning(captchaType),
|
|
56
|
+
}));
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const lastMountOf = (widget) => {
|
|
60
|
+
const mount = mocks.mounts.filter((m) => m.widget === widget).at(-1);
|
|
61
|
+
if (!mount)
|
|
62
|
+
throw new Error(`expected the ${widget} widget to have mounted`);
|
|
63
|
+
return mount;
|
|
64
|
+
};
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
mocks.mounts.length = 0;
|
|
67
|
+
container = document.createElement("div");
|
|
68
|
+
document.body.appendChild(container);
|
|
69
|
+
act(() => {
|
|
70
|
+
root = createRoot(container);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
act(() => {
|
|
75
|
+
root.unmount();
|
|
76
|
+
});
|
|
77
|
+
container.remove();
|
|
78
|
+
vi.clearAllMocks();
|
|
79
|
+
});
|
|
80
|
+
describe("client session id across a PoW escalation", () => {
|
|
81
|
+
it("reaches the PoW widget on the first mount", async () => {
|
|
82
|
+
await mountWrapper(CaptchaType.pow);
|
|
83
|
+
expect(lastMountOf("pow").props.config.clientSessionId).toBe(CLIENT_SESSION_ID);
|
|
84
|
+
});
|
|
85
|
+
it("survives the handoff to the escalated image widget", async () => {
|
|
86
|
+
await mountWrapper(CaptchaType.pow);
|
|
87
|
+
const { onEscalate } = lastMountOf("pow").props;
|
|
88
|
+
await act(async () => {
|
|
89
|
+
onEscalate?.(CaptchaType.image, ESCALATED_SESSION_ID);
|
|
90
|
+
});
|
|
91
|
+
expect(lastMountOf("image").props.config.clientSessionId).toBe(CLIENT_SESSION_ID);
|
|
92
|
+
});
|
|
93
|
+
it("survives the handoff to the escalated puzzle widget", async () => {
|
|
94
|
+
await mountWrapper(CaptchaType.pow);
|
|
95
|
+
const { onEscalate } = lastMountOf("pow").props;
|
|
96
|
+
await act(async () => {
|
|
97
|
+
onEscalate?.(CaptchaType.puzzle, ESCALATED_SESSION_ID);
|
|
98
|
+
});
|
|
99
|
+
expect(lastMountOf("puzzle").props.config.clientSessionId).toBe(CLIENT_SESSION_ID);
|
|
100
|
+
});
|
|
101
|
+
it("swaps the provider session id but not the client one", async () => {
|
|
102
|
+
await mountWrapper(CaptchaType.pow);
|
|
103
|
+
const powState = lastMountOf("pow").props
|
|
104
|
+
.frictionlessState;
|
|
105
|
+
expect(powState.sessionId).toBe(FIRST_SESSION_ID);
|
|
106
|
+
const { onEscalate } = lastMountOf("pow").props;
|
|
107
|
+
await act(async () => {
|
|
108
|
+
onEscalate?.(CaptchaType.image, ESCALATED_SESSION_ID);
|
|
109
|
+
});
|
|
110
|
+
const imageMount = lastMountOf("image").props;
|
|
111
|
+
const imageState = imageMount.frictionlessState;
|
|
112
|
+
expect(imageState.sessionId).toBe(ESCALATED_SESSION_ID);
|
|
113
|
+
expect(imageMount.config.clientSessionId).toBe(CLIENT_SESSION_ID);
|
|
114
|
+
});
|
|
115
|
+
it("carries no client session id when the site rendered without one", async () => {
|
|
116
|
+
await act(async () => {
|
|
117
|
+
root.render(createElement(ProcaptchaFrictionless, {
|
|
118
|
+
config: { ...config(), clientSessionId: undefined },
|
|
119
|
+
callbacks: {},
|
|
120
|
+
restart: vi.fn(),
|
|
121
|
+
i18n: i18nStub,
|
|
122
|
+
detectBot: detectBotReturning(CaptchaType.pow),
|
|
123
|
+
}));
|
|
124
|
+
});
|
|
125
|
+
const { onEscalate } = lastMountOf("pow").props;
|
|
126
|
+
await act(async () => {
|
|
127
|
+
onEscalate?.(CaptchaType.image, ESCALATED_SESSION_ID);
|
|
128
|
+
});
|
|
129
|
+
expect(lastMountOf("image").props.config.clientSessionId).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
//# sourceMappingURL=escalationClientSession.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escalationClientSession.test.js","sourceRoot":"","sources":["../../src/tests/escalationClientSession.test.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prosopo/procaptcha-frictionless",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.18",
|
|
4
4
|
"author": "PROSOPO LIMITED <info@prosopo.io>",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
},
|
|
31
31
|
"browserslist": ["> 0.5%, last 2 versions, not dead"],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@prosopo/api": "4.0
|
|
34
|
-
"@prosopo/common": "3.1.
|
|
35
|
-
"@prosopo/locale": "3.
|
|
36
|
-
"@prosopo/procaptcha-common": "2.
|
|
37
|
-
"@prosopo/procaptcha-pow": "2.
|
|
38
|
-
"@prosopo/procaptcha-puzzle": "2.
|
|
39
|
-
"@prosopo/procaptcha-react": "2.9.
|
|
40
|
-
"@prosopo/types": "5.
|
|
33
|
+
"@prosopo/api": "4.1.0",
|
|
34
|
+
"@prosopo/common": "3.1.52",
|
|
35
|
+
"@prosopo/locale": "3.4.0",
|
|
36
|
+
"@prosopo/procaptcha-common": "2.12.0",
|
|
37
|
+
"@prosopo/procaptcha-pow": "2.11.0",
|
|
38
|
+
"@prosopo/procaptcha-puzzle": "2.11.0",
|
|
39
|
+
"@prosopo/procaptcha-react": "2.9.116",
|
|
40
|
+
"@prosopo/types": "5.4.0",
|
|
41
41
|
"@prosopo/widget-skeleton": "2.8.6",
|
|
42
42
|
"dotenv": "16.4.5",
|
|
43
43
|
"react": "19.2.8"
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"concurrently": "9.0.1",
|
|
50
50
|
"del-cli": "6.0.0",
|
|
51
51
|
"npm-run-all": "4.1.5",
|
|
52
|
+
"react-dom": "19.2.8",
|
|
52
53
|
"tslib": "2.7.0",
|
|
53
54
|
"tsx": "4.20.3",
|
|
54
55
|
"typescript": "5.6.2",
|
|
@@ -0,0 +1,225 @@
|
|
|
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 type { Ti18n } from "@prosopo/locale";
|
|
16
|
+
import {
|
|
17
|
+
CaptchaType,
|
|
18
|
+
type FrictionlessState,
|
|
19
|
+
ModeEnum,
|
|
20
|
+
type ProcaptchaClientConfigInput,
|
|
21
|
+
type ProcaptchaProps,
|
|
22
|
+
type RandomProvider,
|
|
23
|
+
} from "@prosopo/types";
|
|
24
|
+
import { type ReactElement, act, createElement } from "react";
|
|
25
|
+
import { type Root, createRoot } from "react-dom/client";
|
|
26
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
27
|
+
|
|
28
|
+
declare global {
|
|
29
|
+
var IS_REACT_ACT_ENVIRONMENT: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// React only lets act() drive its scheduler when the environment says so;
|
|
33
|
+
// without this every render warns and updates flush unpredictably.
|
|
34
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
35
|
+
|
|
36
|
+
// Every widget the frictionless wrapper can mount records the props it was
|
|
37
|
+
// handed, so a test can assert on the config that reached the *second* mount
|
|
38
|
+
// after an escalation without rendering a real captcha.
|
|
39
|
+
const mocks = vi.hoisted(() => ({
|
|
40
|
+
mounts: [] as {
|
|
41
|
+
widget: "pow" | "image" | "puzzle";
|
|
42
|
+
props: ProcaptchaProps;
|
|
43
|
+
}[],
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const stub =
|
|
47
|
+
(widget: "pow" | "image" | "puzzle") => (props: ProcaptchaProps) => {
|
|
48
|
+
mocks.mounts.push({ widget, props });
|
|
49
|
+
return createElement("div", { "data-widget": widget });
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
vi.mock("@prosopo/procaptcha-pow", () => ({ ProcaptchaPow: stub("pow") }));
|
|
53
|
+
vi.mock("@prosopo/procaptcha-react", () => ({ Procaptcha: stub("image") }));
|
|
54
|
+
vi.mock("@prosopo/procaptcha-puzzle", () => ({
|
|
55
|
+
ProcaptchaPuzzle: stub("puzzle"),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
// The wrapper refuses to run outside a secure context, and jsdom's
|
|
59
|
+
// `isSecureContext` is not reliably true across environments. Everything else
|
|
60
|
+
// from procaptcha-common (Checkbox, getDefaultEvents, providerRetry) is the
|
|
61
|
+
// real implementation.
|
|
62
|
+
vi.mock("@prosopo/procaptcha-common", async (importOriginal) => {
|
|
63
|
+
const actual =
|
|
64
|
+
await importOriginal<typeof import("@prosopo/procaptcha-common")>();
|
|
65
|
+
return { ...actual, isSecureBrowserContext: () => true };
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Imported after the mocks so the lazy loaders inside resolve to the stubs.
|
|
69
|
+
const { ProcaptchaFrictionless } = await import("../ProcaptchaFrictionless.js");
|
|
70
|
+
|
|
71
|
+
const SITE_KEY = "5siteKey";
|
|
72
|
+
const CLIENT_SESSION_ID = "jti-from-the-site";
|
|
73
|
+
const FIRST_SESSION_ID = "provider-session-1";
|
|
74
|
+
const ESCALATED_SESSION_ID = "provider-session-2";
|
|
75
|
+
|
|
76
|
+
const config = (): ProcaptchaClientConfigInput => ({
|
|
77
|
+
account: { address: SITE_KEY },
|
|
78
|
+
userAccountAddress: "",
|
|
79
|
+
web2: true,
|
|
80
|
+
mode: ModeEnum.visible,
|
|
81
|
+
// What the site set via data-sessionid / renderOptions.sessionId.
|
|
82
|
+
clientSessionId: CLIENT_SESSION_ID,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const i18nStub = {
|
|
86
|
+
isInitialized: true,
|
|
87
|
+
language: "en",
|
|
88
|
+
t: (key: string) => key,
|
|
89
|
+
changeLanguage: vi.fn(),
|
|
90
|
+
} as unknown as Ti18n;
|
|
91
|
+
|
|
92
|
+
// Minimal shape of what customDetectBot resolves to; only the fields the
|
|
93
|
+
// wrapper reads on the happy path need to be present.
|
|
94
|
+
const detectBotReturning = (captchaType: CaptchaType) =>
|
|
95
|
+
vi.fn().mockResolvedValue({
|
|
96
|
+
captchaType,
|
|
97
|
+
sessionId: FIRST_SESSION_ID,
|
|
98
|
+
provider: { provider: { url: "https://provider.test" } } as RandomProvider,
|
|
99
|
+
userAccount: "userAccount",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
let container: HTMLDivElement;
|
|
103
|
+
let root: Root;
|
|
104
|
+
|
|
105
|
+
const mountWrapper = async (captchaType: CaptchaType): Promise<void> => {
|
|
106
|
+
await act(async () => {
|
|
107
|
+
root.render(
|
|
108
|
+
createElement(ProcaptchaFrictionless, {
|
|
109
|
+
config: config(),
|
|
110
|
+
callbacks: {},
|
|
111
|
+
restart: vi.fn(),
|
|
112
|
+
i18n: i18nStub,
|
|
113
|
+
detectBot: detectBotReturning(captchaType),
|
|
114
|
+
}) as ReactElement,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const lastMountOf = (widget: "pow" | "image" | "puzzle") => {
|
|
120
|
+
const mount = mocks.mounts.filter((m) => m.widget === widget).at(-1);
|
|
121
|
+
if (!mount) throw new Error(`expected the ${widget} widget to have mounted`);
|
|
122
|
+
return mount;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
beforeEach(() => {
|
|
126
|
+
mocks.mounts.length = 0;
|
|
127
|
+
container = document.createElement("div");
|
|
128
|
+
document.body.appendChild(container);
|
|
129
|
+
act(() => {
|
|
130
|
+
root = createRoot(container);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
act(() => {
|
|
136
|
+
root.unmount();
|
|
137
|
+
});
|
|
138
|
+
container.remove();
|
|
139
|
+
vi.clearAllMocks();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// The PoW -> image/puzzle escalation is the one flow where a user solves a
|
|
143
|
+
// second captcha under a *different* provider session. The client session id
|
|
144
|
+
// has to survive that handoff, or the dapp server's verify call would be
|
|
145
|
+
// correlated against an escalated record that never carried it and every
|
|
146
|
+
// escalated user would be rejected.
|
|
147
|
+
describe("client session id across a PoW escalation", () => {
|
|
148
|
+
it("reaches the PoW widget on the first mount", async () => {
|
|
149
|
+
await mountWrapper(CaptchaType.pow);
|
|
150
|
+
|
|
151
|
+
expect(lastMountOf("pow").props.config.clientSessionId).toBe(
|
|
152
|
+
CLIENT_SESSION_ID,
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("survives the handoff to the escalated image widget", async () => {
|
|
157
|
+
await mountWrapper(CaptchaType.pow);
|
|
158
|
+
|
|
159
|
+
// The provider accepted the PoW but wants a visual challenge; the PoW
|
|
160
|
+
// widget calls back with the replacement session the provider minted.
|
|
161
|
+
const { onEscalate } = lastMountOf("pow").props;
|
|
162
|
+
await act(async () => {
|
|
163
|
+
onEscalate?.(CaptchaType.image, ESCALATED_SESSION_ID);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(lastMountOf("image").props.config.clientSessionId).toBe(
|
|
167
|
+
CLIENT_SESSION_ID,
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("survives the handoff to the escalated puzzle widget", async () => {
|
|
172
|
+
await mountWrapper(CaptchaType.pow);
|
|
173
|
+
|
|
174
|
+
const { onEscalate } = lastMountOf("pow").props;
|
|
175
|
+
await act(async () => {
|
|
176
|
+
onEscalate?.(CaptchaType.puzzle, ESCALATED_SESSION_ID);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(lastMountOf("puzzle").props.config.clientSessionId).toBe(
|
|
180
|
+
CLIENT_SESSION_ID,
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("swaps the provider session id but not the client one", async () => {
|
|
185
|
+
await mountWrapper(CaptchaType.pow);
|
|
186
|
+
|
|
187
|
+
const powState = lastMountOf("pow").props
|
|
188
|
+
.frictionlessState as FrictionlessState;
|
|
189
|
+
expect(powState.sessionId).toBe(FIRST_SESSION_ID);
|
|
190
|
+
|
|
191
|
+
const { onEscalate } = lastMountOf("pow").props;
|
|
192
|
+
await act(async () => {
|
|
193
|
+
onEscalate?.(CaptchaType.image, ESCALATED_SESSION_ID);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const imageMount = lastMountOf("image").props;
|
|
197
|
+
const imageState = imageMount.frictionlessState as FrictionlessState;
|
|
198
|
+
// These are different identifiers and must not be conflated: the
|
|
199
|
+
// provider session rotates on escalation, the site's own session does
|
|
200
|
+
// not. The verify-time correlation keys off the latter.
|
|
201
|
+
expect(imageState.sessionId).toBe(ESCALATED_SESSION_ID);
|
|
202
|
+
expect(imageMount.config.clientSessionId).toBe(CLIENT_SESSION_ID);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("carries no client session id when the site rendered without one", async () => {
|
|
206
|
+
await act(async () => {
|
|
207
|
+
root.render(
|
|
208
|
+
createElement(ProcaptchaFrictionless, {
|
|
209
|
+
config: { ...config(), clientSessionId: undefined },
|
|
210
|
+
callbacks: {},
|
|
211
|
+
restart: vi.fn(),
|
|
212
|
+
i18n: i18nStub,
|
|
213
|
+
detectBot: detectBotReturning(CaptchaType.pow),
|
|
214
|
+
}) as ReactElement,
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const { onEscalate } = lastMountOf("pow").props;
|
|
219
|
+
await act(async () => {
|
|
220
|
+
onEscalate?.(CaptchaType.image, ESCALATED_SESSION_ID);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(lastMountOf("image").props.config.clientSessionId).toBeUndefined();
|
|
224
|
+
});
|
|
225
|
+
});
|