@prosopo/procaptcha-puzzle 2.10.40 → 2.10.42

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 (52) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +4 -4
  2. package/.turbo/turbo-build$colon$tsc.log +19 -19
  3. package/.turbo/turbo-build.log +6 -6
  4. package/CHANGELOG.md +12 -0
  5. package/dist/cjs/components/ProcaptchaWidget.cjs +10 -5
  6. package/dist/cjs/services/Manager.cjs +3 -3
  7. package/dist/components/ProcaptchaWidget.d.ts.map +1 -1
  8. package/dist/components/ProcaptchaWidget.js +10 -5
  9. package/dist/components/ProcaptchaWidget.js.map +1 -1
  10. package/dist/services/Manager.d.ts.map +1 -1
  11. package/dist/services/Manager.js +3 -3
  12. package/dist/services/Manager.js.map +1 -1
  13. package/dist/tests/manager.unit.test.d.ts +2 -0
  14. package/dist/tests/manager.unit.test.d.ts.map +1 -0
  15. package/dist/tests/manager.unit.test.js +506 -0
  16. package/dist/tests/manager.unit.test.js.map +1 -0
  17. package/dist/tests/managerHarness.d.ts +20 -0
  18. package/dist/tests/managerHarness.d.ts.map +1 -0
  19. package/dist/tests/managerHarness.js +86 -0
  20. package/dist/tests/managerHarness.js.map +1 -0
  21. package/dist/tests/procaptchaPuzzle.test-d.d.ts +2 -0
  22. package/dist/tests/procaptchaPuzzle.test-d.d.ts.map +1 -0
  23. package/dist/tests/procaptchaPuzzle.test-d.js +97 -0
  24. package/dist/tests/procaptchaPuzzle.test-d.js.map +1 -0
  25. package/dist/tests/procaptchaPuzzle.unit.test.d.ts +2 -0
  26. package/dist/tests/procaptchaPuzzle.unit.test.d.ts.map +1 -0
  27. package/dist/tests/procaptchaPuzzle.unit.test.js +81 -0
  28. package/dist/tests/procaptchaPuzzle.unit.test.js.map +1 -0
  29. package/dist/tests/procaptchaWidget.unit.test.d.ts +2 -0
  30. package/dist/tests/procaptchaWidget.unit.test.d.ts.map +1 -0
  31. package/dist/tests/procaptchaWidget.unit.test.js +547 -0
  32. package/dist/tests/procaptchaWidget.unit.test.js.map +1 -0
  33. package/dist/tests/puzzleCanvas.unit.test.d.ts +2 -0
  34. package/dist/tests/puzzleCanvas.unit.test.d.ts.map +1 -0
  35. package/dist/tests/puzzleCanvas.unit.test.js +312 -0
  36. package/dist/tests/puzzleCanvas.unit.test.js.map +1 -0
  37. package/dist/tests/setup.d.ts +5 -0
  38. package/dist/tests/setup.d.ts.map +1 -0
  39. package/dist/tests/setup.js +3 -0
  40. package/dist/tests/setup.js.map +1 -0
  41. package/package.json +10 -5
  42. package/src/components/ProcaptchaWidget.tsx +17 -6
  43. package/src/services/Manager.ts +10 -6
  44. package/src/tests/manager.unit.test.ts +763 -0
  45. package/src/tests/managerHarness.ts +169 -0
  46. package/src/tests/procaptchaPuzzle.test-d.ts +191 -0
  47. package/src/tests/procaptchaPuzzle.unit.test.ts +123 -0
  48. package/src/tests/procaptchaWidget.unit.test.ts +789 -0
  49. package/src/tests/puzzleCanvas.unit.test.ts +418 -0
  50. package/src/tests/setup.ts +26 -0
  51. package/tsconfig.tsbuildinfo +1 -1
  52. package/vite.test.config.ts +27 -0
@@ -0,0 +1,169 @@
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 {
16
+ Account,
17
+ FrictionlessState,
18
+ GetPuzzleCaptchaResponse,
19
+ MouseMovementPoint,
20
+ ProcaptchaCallbacks,
21
+ ProcaptchaClientConfigInput,
22
+ ProcaptchaState,
23
+ PuzzleCaptchaSolutionResponse,
24
+ PuzzleEvent,
25
+ RandomProvider,
26
+ } from "@prosopo/types";
27
+ import { vi } from "vitest";
28
+
29
+ /**
30
+ * Shared fixtures for the puzzle suites. Kept out of the test files so the mock
31
+ * factories, which vitest hoists above every import, can build their canned
32
+ * responses from the same shapes the assertions use.
33
+ */
34
+
35
+ export const PROVIDER_URL = "https://provider.one";
36
+ export const OTHER_PROVIDER_URL = "https://provider.two";
37
+ export const USER_ADDRESS = "user-address";
38
+ export const SITE_KEY = "site-key";
39
+
40
+ export const config = (
41
+ overrides: Partial<ProcaptchaClientConfigInput> = {},
42
+ ): ProcaptchaClientConfigInput => ({
43
+ account: { address: SITE_KEY },
44
+ defaultEnvironment: "production",
45
+ ...overrides,
46
+ });
47
+
48
+ export const state = (
49
+ overrides: Partial<ProcaptchaState> = {},
50
+ ): ProcaptchaState => ({
51
+ isHuman: false,
52
+ index: 0,
53
+ solutions: [],
54
+ captchaApi: undefined,
55
+ challenge: undefined,
56
+ showModal: false,
57
+ loading: false,
58
+ account: undefined,
59
+ dappAccount: undefined,
60
+ submission: undefined,
61
+ timeout: undefined,
62
+ successfullChallengeTimeout: undefined,
63
+ sendData: false,
64
+ attemptCount: 0,
65
+ error: undefined,
66
+ sessionId: undefined,
67
+ ...overrides,
68
+ });
69
+
70
+ export type SignRaw = NonNullable<
71
+ NonNullable<Account["extension"]>["signer"]["signRaw"]
72
+ >;
73
+
74
+ /**
75
+ * An account carrying just enough of an injected extension to sign: the rest of
76
+ * the interface is never touched by the manager, but the type demands it.
77
+ */
78
+ export const account = (signRaw?: SignRaw): Account => ({
79
+ account: { address: USER_ADDRESS },
80
+ extension: {
81
+ name: "test-extension",
82
+ version: "0.0.0",
83
+ accounts: {
84
+ get: async () => [{ address: USER_ADDRESS }],
85
+ subscribe: () => () => undefined,
86
+ },
87
+ signer: signRaw ? { signRaw } : {},
88
+ },
89
+ });
90
+
91
+ export const accountWithoutExtension = (): Account => ({
92
+ account: { address: USER_ADDRESS },
93
+ });
94
+
95
+ export const randomProvider = (url: string = PROVIDER_URL): RandomProvider => ({
96
+ providerAccount: "provider-account",
97
+ provider: { url },
98
+ });
99
+
100
+ export const challengeResponse = (
101
+ overrides: Partial<GetPuzzleCaptchaResponse> = {},
102
+ ): GetPuzzleCaptchaResponse => ({
103
+ challenge: "0x1___0xdeadbeef___1700000000000",
104
+ targetX: 200,
105
+ targetY: 80,
106
+ originX: 20,
107
+ originY: 100,
108
+ tolerance: 10,
109
+ timestamp: "1700000000000",
110
+ signature: { provider: { challenge: "0xprovider-challenge" } },
111
+ status: "ok",
112
+ ...overrides,
113
+ });
114
+
115
+ export const solutionResponse = (
116
+ overrides: Partial<PuzzleCaptchaSolutionResponse> = {},
117
+ ): PuzzleCaptchaSolutionResponse => ({
118
+ verified: true,
119
+ status: "ok",
120
+ ...overrides,
121
+ });
122
+
123
+ export const puzzleEvents = (): PuzzleEvent[] => [
124
+ { x: 20, y: 100, t: 0 },
125
+ { x: 120, y: 90, t: 120 },
126
+ { x: 200, y: 80, t: 260 },
127
+ ];
128
+
129
+ export const callbacks = (
130
+ overrides: Partial<ProcaptchaCallbacks> = {},
131
+ ): ProcaptchaCallbacks => ({ ...overrides });
132
+
133
+ /**
134
+ * The signer every fixture account uses, so a test can assert on what the
135
+ * manager asked the extension to sign without rebuilding the frictionless
136
+ * state it was handed.
137
+ */
138
+ export const signRawMock = vi.fn<SignRaw>();
139
+
140
+ export const frictionless = (
141
+ overrides: Partial<FrictionlessState> = {},
142
+ ): FrictionlessState => ({
143
+ provider: randomProvider(),
144
+ userAccount: account(signRawMock),
145
+ restart: () => undefined,
146
+ ...overrides,
147
+ });
148
+
149
+ /**
150
+ * A behaviour collector holding a fixed set of points. The manager only ever
151
+ * calls `getData`, but the type demands the whole lifecycle, so the rest are
152
+ * stubs rather than omissions.
153
+ */
154
+ export const collector = (
155
+ points: MouseMovementPoint[],
156
+ ): NonNullable<FrictionlessState["behaviorCollector1"]> => ({
157
+ start: () => undefined,
158
+ stop: () => undefined,
159
+ getData: () => points,
160
+ clear: () => undefined,
161
+ });
162
+
163
+ /**
164
+ * jsdom's setTimeout hands back a plain number while the shared state types the
165
+ * handle as Node's Timeout, so tests that seed a pending timer have to bridge
166
+ * the two. The value the manager passes to clearTimeout is the number itself.
167
+ */
168
+ export const timerHandle = (id: number): ReturnType<typeof setTimeout> =>
169
+ id as unknown as ReturnType<typeof setTimeout>;
@@ -0,0 +1,191 @@
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 type {
17
+ FrictionlessState,
18
+ GetPuzzleCaptchaResponse,
19
+ ProcaptchaCallbacks,
20
+ ProcaptchaClientConfigInput,
21
+ ProcaptchaProps,
22
+ ProcaptchaState,
23
+ ProcaptchaStateUpdateFn,
24
+ PuzzleEvent,
25
+ } from "@prosopo/types";
26
+ import type { ReactElement } from "react";
27
+ import { assertType, describe, expectTypeOf, test } from "vitest";
28
+ import { PuzzleCanvas } from "../components/PuzzleCanvas.js";
29
+ import type * as entrypoint from "../index.js";
30
+ import { ProcaptchaPuzzle } from "../index.js";
31
+ import { Manager } from "../services/Manager.js";
32
+ import {
33
+ challengeResponse,
34
+ config,
35
+ frictionless,
36
+ puzzleEvents,
37
+ state,
38
+ } from "./managerHarness.js";
39
+
40
+ /** The widget only ever reads `language`/`changeLanguage` off this. */
41
+ const i18n = (): Ti18n => undefined as unknown as Ti18n;
42
+
43
+ describe("the package entrypoint's types", () => {
44
+ test("ProcaptchaPuzzle takes the shared widget props and renders an element", () => {
45
+ expectTypeOf(ProcaptchaPuzzle).parameters.toEqualTypeOf<
46
+ [ProcaptchaProps]
47
+ >();
48
+ expectTypeOf(ProcaptchaPuzzle).returns.toExtend<ReactElement>();
49
+ });
50
+
51
+ test("the inner widget's default export is not re-exported", () => {
52
+ // `export *` skips default exports, so consumers can only reach the lazy
53
+ // wrapper — the one that works without a code-splitting bundler.
54
+ expectTypeOf<keyof typeof entrypoint>().toEqualTypeOf<"ProcaptchaPuzzle">();
55
+ });
56
+
57
+ test("config, callbacks and i18n are all required", () => {
58
+ // @ts-expect-error - a widget with no config has no provider to talk to.
59
+ assertType<ProcaptchaProps>({ callbacks: {}, i18n: i18n() });
60
+ // @ts-expect-error - callbacks decide what a solve reports back.
61
+ assertType<ProcaptchaProps>({ config: config(), i18n: i18n() });
62
+ assertType<ProcaptchaProps>({
63
+ config: config(),
64
+ callbacks: {},
65
+ i18n: i18n(),
66
+ });
67
+ });
68
+ });
69
+
70
+ describe("Manager's types", () => {
71
+ const updateState: ProcaptchaStateUpdateFn = () => undefined;
72
+ const callbacks: ProcaptchaCallbacks = {};
73
+
74
+ test("only the first four arguments are required", () => {
75
+ assertType<ReturnType<typeof Manager>>(
76
+ Manager(config(), state(), updateState, callbacks),
77
+ );
78
+ // @ts-expect-error - callbacks decide what a solve reports back.
79
+ Manager(config(), state(), updateState);
80
+ });
81
+
82
+ test("the optional arguments keep their positions", () => {
83
+ expectTypeOf(Manager)
84
+ .parameter(4)
85
+ .toEqualTypeOf<FrictionlessState | undefined>();
86
+ expectTypeOf(Manager)
87
+ .parameter(5)
88
+ .toEqualTypeOf<(() => string | undefined) | undefined>();
89
+ });
90
+
91
+ test("it exposes exactly start, submitSolution and resetState", () => {
92
+ expectTypeOf<keyof ReturnType<typeof Manager>>().toEqualTypeOf<
93
+ "start" | "submitSolution" | "resetState"
94
+ >();
95
+ });
96
+
97
+ test("start hands back the challenge the canvas needs to draw", () => {
98
+ // Unlike the POW manager, which reports only through state, the puzzle
99
+ // manager returns the challenge — the widget cannot render a board
100
+ // without the origin/target coordinates.
101
+ expectTypeOf<ReturnType<typeof Manager>["start"]>().toEqualTypeOf<
102
+ (x?: number, y?: number) => Promise<GetPuzzleCaptchaResponse | undefined>
103
+ >();
104
+ });
105
+
106
+ test("submitSolution takes the drop point and the full event trail", () => {
107
+ expectTypeOf<
108
+ Parameters<ReturnType<typeof Manager>["submitSolution"]>
109
+ >().toEqualTypeOf<
110
+ [finalX: number, finalY: number, events: PuzzleEvent[]]
111
+ >();
112
+ expectTypeOf<
113
+ ReturnType<ReturnType<typeof Manager>["submitSolution"]>
114
+ >().toEqualTypeOf<Promise<boolean>>();
115
+ });
116
+
117
+ test("resetState takes the frictionless restart callback and nothing else", () => {
118
+ expectTypeOf<
119
+ Parameters<ReturnType<typeof Manager>["resetState"]>
120
+ >().toEqualTypeOf<[frictionlessRestart?: (() => void) | undefined]>();
121
+ expectTypeOf<
122
+ ReturnType<ReturnType<typeof Manager>["resetState"]>
123
+ >().toEqualTypeOf<void>();
124
+ });
125
+
126
+ test("coordinates come off DOM events, so they are numbers", () => {
127
+ const manager: ReturnType<typeof Manager> = Manager(
128
+ config(),
129
+ state(),
130
+ updateState,
131
+ callbacks,
132
+ frictionless(),
133
+ );
134
+ // @ts-expect-error - never strings, whatever the DOM stringifies to.
135
+ manager.start("1", "2");
136
+ // @ts-expect-error - the trail is required; an empty drag still sends [].
137
+ manager.submitSolution(1, 2);
138
+ });
139
+ });
140
+
141
+ describe("PuzzleCanvas' types", () => {
142
+ const onComplete = (
143
+ _finalX: number,
144
+ _finalY: number,
145
+ _events: PuzzleEvent[],
146
+ ): void => undefined;
147
+
148
+ test("every prop is required, since none has a sensible default", () => {
149
+ // @ts-expect-error - a board with no target cannot be solved.
150
+ PuzzleCanvas({ originX: 0, originY: 0 });
151
+ // @ts-expect-error - `submitting` gates the drag; omitting it unlocks it.
152
+ PuzzleCanvas({
153
+ originX: 0,
154
+ originY: 0,
155
+ targetX: 1,
156
+ targetY: 1,
157
+ onComplete,
158
+ showRetry: false,
159
+ });
160
+ });
161
+
162
+ test("the full prop set renders an element", () => {
163
+ expectTypeOf(
164
+ PuzzleCanvas({
165
+ originX: 0,
166
+ originY: 0,
167
+ targetX: 1,
168
+ targetY: 1,
169
+ onComplete,
170
+ showRetry: false,
171
+ submitting: false,
172
+ }),
173
+ ).toExtend<ReactElement>();
174
+ });
175
+
176
+ test("the drop is reported synchronously, not as a promise", () => {
177
+ // The widget's own handler is async, but the canvas must not await it:
178
+ // a returned promise here would be dropped on the floor.
179
+ expectTypeOf(onComplete).returns.toEqualTypeOf<void>();
180
+ });
181
+ });
182
+
183
+ describe("the harness fixtures match the shared types", () => {
184
+ test("they build the real shapes, not lookalikes", () => {
185
+ expectTypeOf(config()).toEqualTypeOf<ProcaptchaClientConfigInput>();
186
+ expectTypeOf(state()).toEqualTypeOf<ProcaptchaState>();
187
+ expectTypeOf(frictionless()).toEqualTypeOf<FrictionlessState>();
188
+ expectTypeOf(challengeResponse()).toEqualTypeOf<GetPuzzleCaptchaResponse>();
189
+ expectTypeOf(puzzleEvents()).toEqualTypeOf<PuzzleEvent[]>();
190
+ });
191
+ });
@@ -0,0 +1,123 @@
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 type { ProcaptchaProps } from "@prosopo/types";
17
+ import { type ReactElement, act, createElement } from "react";
18
+ import { type Root, createRoot } from "react-dom/client";
19
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
20
+ import * as entrypoint from "../index.js";
21
+ import { config } from "./managerHarness.js";
22
+
23
+ /**
24
+ * The outer component exists only to lazy-load the widget and hand it the props
25
+ * it was given, so what is worth pinning down is that nothing is dropped on the
26
+ * way through — a wrapper that names props one at a time silently loses the
27
+ * ones added later.
28
+ */
29
+ const mocks = vi.hoisted(() => ({
30
+ received: [] as ProcaptchaProps[],
31
+ }));
32
+
33
+ vi.mock("../components/ProcaptchaWidget.js", async () => {
34
+ const { createElement: create } = await import("react");
35
+ return {
36
+ default: (props: ProcaptchaProps) => {
37
+ mocks.received.push(props);
38
+ return create("div", { "data-testid": "widget" });
39
+ },
40
+ };
41
+ });
42
+
43
+ let container: HTMLDivElement;
44
+ let root: Root;
45
+
46
+ const props = (overrides: Partial<ProcaptchaProps> = {}): ProcaptchaProps => ({
47
+ config: config(),
48
+ callbacks: {},
49
+ i18n: undefined as unknown as Ti18n,
50
+ ...overrides,
51
+ });
52
+
53
+ const render = async (widgetProps: ProcaptchaProps): Promise<void> => {
54
+ await act(async () => {
55
+ root.render(
56
+ createElement(entrypoint.ProcaptchaPuzzle, widgetProps) as ReactElement,
57
+ );
58
+ });
59
+ };
60
+
61
+ beforeEach(() => {
62
+ mocks.received.length = 0;
63
+ container = document.createElement("div");
64
+ document.body.appendChild(container);
65
+ act(() => {
66
+ root = createRoot(container);
67
+ });
68
+ });
69
+
70
+ afterEach(() => {
71
+ act(() => {
72
+ root.unmount();
73
+ });
74
+ container.remove();
75
+ });
76
+
77
+ describe("ProcaptchaPuzzle", () => {
78
+ test("renders the widget once it has loaded", async () => {
79
+ await render(props());
80
+ expect(container.querySelector('[data-testid="widget"]')).not.toBeNull();
81
+ });
82
+
83
+ test("passes on the props the widget needs but never names", async () => {
84
+ const onSessionInvalidated = vi.fn<(x?: number, y?: number) => void>();
85
+ await render(
86
+ props({
87
+ autoStart: true,
88
+ startCoords: { x: 1, y: 2 },
89
+ onSessionInvalidated,
90
+ }),
91
+ );
92
+ expect(mocks.received[0]).toMatchObject({
93
+ autoStart: true,
94
+ startCoords: { x: 1, y: 2 },
95
+ onSessionInvalidated,
96
+ });
97
+ });
98
+
99
+ test("passes on the props it does name, too", async () => {
100
+ const callbacks = { onHuman: vi.fn<(token: string) => void>() };
101
+ await render(props({ callbacks }));
102
+ expect(mocks.received[0]?.callbacks).toBe(callbacks);
103
+ expect(mocks.received[0]?.config).toEqual(config());
104
+ });
105
+
106
+ test("mounts one widget per render, not one per prop change", async () => {
107
+ await render(props());
108
+ await render(props({ autoStart: true }));
109
+ expect(container.querySelectorAll('[data-testid="widget"]')).toHaveLength(
110
+ 1,
111
+ );
112
+ });
113
+ });
114
+
115
+ describe("the package entrypoint", () => {
116
+ test("exports the wrapper a consumer mounts", () => {
117
+ // The inner ProcaptchaWidget is a default export, which `export *` does
118
+ // not re-export: consumers get the lazy-loading wrapper only, which is
119
+ // the one that works outside a bundler that can code-split.
120
+ expect(typeof entrypoint.ProcaptchaPuzzle).toBe("function");
121
+ expect(Object.keys(entrypoint)).toEqual(["ProcaptchaPuzzle"]);
122
+ });
123
+ });