@prosopo/procaptcha-bundle 4.2.5 → 4.4.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.
Files changed (59) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +16 -15
  2. package/.turbo/turbo-build$colon$tsc.log +26 -26
  3. package/.turbo/turbo-build.log +16 -15
  4. package/CHANGELOG.md +85 -0
  5. package/dist/cjs/index.cjs +101 -21
  6. package/dist/cjs/util/captcha/captchaRenderer.cjs +20 -1
  7. package/dist/cjs/util/configCreator.cjs +4 -2
  8. package/dist/cjs/util/startMode.cjs +20 -0
  9. package/dist/cjs/util/widgetFactory.cjs +8 -5
  10. package/dist/index.d.ts +4 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +101 -22
  13. package/dist/index.js.map +1 -1
  14. package/dist/tests/bind.unit.test.d.ts +2 -0
  15. package/dist/tests/bind.unit.test.d.ts.map +1 -0
  16. package/dist/tests/bind.unit.test.js +166 -0
  17. package/dist/tests/bind.unit.test.js.map +1 -0
  18. package/dist/tests/detectorPrefetch.unit.test.js.map +1 -1
  19. package/dist/tests/procaptchaReady.unit.test.js +1 -0
  20. package/dist/tests/procaptchaReady.unit.test.js.map +1 -1
  21. package/dist/tests/reset.unit.test.js +6 -2
  22. package/dist/tests/reset.unit.test.js.map +1 -1
  23. package/dist/tests/start.unit.test.d.ts +2 -0
  24. package/dist/tests/start.unit.test.d.ts.map +1 -0
  25. package/dist/tests/start.unit.test.js +103 -0
  26. package/dist/tests/start.unit.test.js.map +1 -0
  27. package/dist/tests/util/startMode.test.d.ts +2 -0
  28. package/dist/tests/util/startMode.test.d.ts.map +1 -0
  29. package/dist/tests/util/startMode.test.js +48 -0
  30. package/dist/tests/util/startMode.test.js.map +1 -0
  31. package/dist/util/captcha/captchaRenderer.d.ts +1 -1
  32. package/dist/util/captcha/captchaRenderer.d.ts.map +1 -1
  33. package/dist/util/captcha/captchaRenderer.js +20 -1
  34. package/dist/util/captcha/captchaRenderer.js.map +1 -1
  35. package/dist/util/configCreator.d.ts +14 -2
  36. package/dist/util/configCreator.d.ts.map +1 -1
  37. package/dist/util/configCreator.js +5 -3
  38. package/dist/util/configCreator.js.map +1 -1
  39. package/dist/util/startMode.d.ts +5 -0
  40. package/dist/util/startMode.d.ts.map +1 -0
  41. package/dist/util/startMode.js +18 -0
  42. package/dist/util/startMode.js.map +1 -0
  43. package/dist/util/widgetFactory.d.ts +7 -2
  44. package/dist/util/widgetFactory.d.ts.map +1 -1
  45. package/dist/util/widgetFactory.js +8 -5
  46. package/dist/util/widgetFactory.js.map +1 -1
  47. package/package.json +12 -12
  48. package/src/index.ts +145 -28
  49. package/src/tests/bind.unit.test.ts +227 -0
  50. package/src/tests/detectorPrefetch.unit.test.ts +2 -2
  51. package/src/tests/procaptchaReady.unit.test.ts +1 -0
  52. package/src/tests/reset.unit.test.ts +9 -3
  53. package/src/tests/start.unit.test.ts +155 -0
  54. package/src/tests/util/startMode.test.ts +113 -0
  55. package/src/util/captcha/captchaRenderer.tsx +33 -13
  56. package/src/util/configCreator.ts +29 -11
  57. package/src/util/startMode.ts +53 -0
  58. package/src/util/widgetFactory.ts +10 -3
  59. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,155 @@
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 {
16
+ PROCAPTCHA_START_EVENT,
17
+ type ProcaptchaStartEventDetail,
18
+ StartModeEnum,
19
+ } from "@prosopo/types";
20
+ import type { Root } from "react-dom/client";
21
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
+ import type { CreatedWidget } from "../util/widgetFactory.js";
23
+
24
+ const mocks = vi.hoisted(() => ({
25
+ prefetchDetector: vi.fn(),
26
+ createWidgets: vi.fn(),
27
+ }));
28
+
29
+ vi.mock("@prosopo/procaptcha-frictionless", () => ({
30
+ prefetchDetector: mocks.prefetchDetector,
31
+ }));
32
+
33
+ vi.mock("@prosopo/procaptcha-common", () => ({
34
+ getWindowCallback: vi.fn(),
35
+ pickIpMode: vi.fn(() => undefined),
36
+ }));
37
+
38
+ vi.mock("../util/widgetFactory.js", () => ({
39
+ WidgetFactory: vi.fn(function () {
40
+ return { createWidgets: mocks.createWidgets };
41
+ }),
42
+ }));
43
+
44
+ const { render, remove, start } = await import("../index.js");
45
+
46
+ const SITE_KEY = "5CcNvLUdiXFpzKDMjThGLSK9rhWHA1H4EF3zrgkpkjAdqmuP";
47
+
48
+ const makeWidget = (): CreatedWidget => ({
49
+ root: { unmount: vi.fn(), render: vi.fn() } as unknown as Root,
50
+ container: document.createElement("div"),
51
+ });
52
+
53
+ const flush = (): Promise<void> =>
54
+ new Promise((resolve) => setTimeout(resolve, 0));
55
+
56
+ const renderWidget = async (
57
+ options: Partial<Parameters<typeof render>[1]> = {},
58
+ ): Promise<{ element: HTMLDivElement; id: string }> => {
59
+ mocks.createWidgets.mockResolvedValueOnce([makeWidget()]);
60
+ const element = document.createElement("div");
61
+ const id = await render(element, { siteKey: SITE_KEY, ...options });
62
+ if (!id) throw new Error("expected render to return a widget id");
63
+ return { element, id };
64
+ };
65
+
66
+ let received: ProcaptchaStartEventDetail[];
67
+ const onStart = (event: Event): void => {
68
+ received.push((event as CustomEvent<ProcaptchaStartEventDetail>).detail);
69
+ };
70
+
71
+ beforeEach(async () => {
72
+ vi.clearAllMocks();
73
+ await remove();
74
+ received = [];
75
+ document.addEventListener(PROCAPTCHA_START_EVENT, onStart);
76
+ });
77
+
78
+ afterEach(() => {
79
+ document.removeEventListener(PROCAPTCHA_START_EVENT, onStart);
80
+ });
81
+
82
+ describe("start", () => {
83
+ it("addresses one event to each widget when no id is given", async () => {
84
+ const first = await renderWidget();
85
+ const second = await renderWidget();
86
+
87
+ start();
88
+
89
+ expect(received.map((detail) => detail.element)).toEqual([
90
+ first.element,
91
+ second.element,
92
+ ]);
93
+ });
94
+
95
+ it("addresses the event to the widget whose id is given", async () => {
96
+ await renderWidget();
97
+ const second = await renderWidget();
98
+
99
+ start(second.id);
100
+
101
+ expect(received).toHaveLength(1);
102
+ expect(received[0]?.element).toBe(second.element);
103
+ });
104
+
105
+ it("reports an unknown id instead of dispatching", async () => {
106
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
107
+ await renderWidget();
108
+
109
+ start("procaptcha-widget-does-not-exist");
110
+
111
+ expect(received).toHaveLength(0);
112
+ expect(error).toHaveBeenCalledTimes(1);
113
+ error.mockRestore();
114
+ });
115
+
116
+ it("reports when there is nothing to start", () => {
117
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
118
+
119
+ start();
120
+
121
+ expect(received).toHaveLength(0);
122
+ expect(error).toHaveBeenCalledTimes(1);
123
+ error.mockRestore();
124
+ });
125
+
126
+ it("is exposed on window.procaptcha", () => {
127
+ expect(window.procaptcha.start).toBe(start);
128
+ });
129
+ });
130
+
131
+ describe("detector prefetch", () => {
132
+ it("runs on render in auto mode", async () => {
133
+ await renderWidget();
134
+ await flush();
135
+
136
+ expect(mocks.prefetchDetector).toHaveBeenCalledTimes(1);
137
+ });
138
+
139
+ it("is skipped in manual mode so nothing reaches the provider on load", async () => {
140
+ await renderWidget({ startMode: StartModeEnum.manual });
141
+ await flush();
142
+
143
+ expect(mocks.prefetchDetector).not.toHaveBeenCalled();
144
+ });
145
+
146
+ it("is skipped when the element asks for manual mode", async () => {
147
+ mocks.createWidgets.mockResolvedValueOnce([makeWidget()]);
148
+ const element = document.createElement("div");
149
+ element.setAttribute("data-start-mode", "manual");
150
+ await render(element, { siteKey: SITE_KEY });
151
+ await flush();
152
+
153
+ expect(mocks.prefetchDetector).not.toHaveBeenCalled();
154
+ });
155
+ });
@@ -0,0 +1,113 @@
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 {
16
+ type ProcaptchaClientConfigOutput,
17
+ ProcaptchaConfigSchema,
18
+ type ProcaptchaRenderOptions,
19
+ StartModeEnum,
20
+ } from "@prosopo/types";
21
+ import { JSDOM } from "jsdom";
22
+ import { beforeEach, describe, expect, it, vi } from "vitest";
23
+ import {
24
+ START_MODE_ATTRIBUTE,
25
+ resolveStartMode,
26
+ setStartMode,
27
+ } from "../../util/startMode.js";
28
+
29
+ const SITE_KEY = "5site";
30
+
31
+ const makeConfig = (): ProcaptchaClientConfigOutput =>
32
+ ProcaptchaConfigSchema.parse({ account: { address: SITE_KEY } });
33
+
34
+ const makeElement = (attributes: Record<string, string> = {}): Element => {
35
+ const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
36
+ const element = dom.window.document.createElement("div");
37
+ for (const [name, value] of Object.entries(attributes)) {
38
+ element.setAttribute(name, value);
39
+ }
40
+ return element;
41
+ };
42
+
43
+ const renderOptions = (
44
+ overrides: Partial<ProcaptchaRenderOptions> = {},
45
+ ): ProcaptchaRenderOptions => ({ siteKey: SITE_KEY, ...overrides });
46
+
47
+ describe("resolveStartMode", () => {
48
+ beforeEach(() => {
49
+ vi.restoreAllMocks();
50
+ });
51
+
52
+ it("defaults to auto", () => {
53
+ expect(resolveStartMode(renderOptions(), makeElement())).toBe(
54
+ StartModeEnum.auto,
55
+ );
56
+ expect(resolveStartMode(undefined, makeElement())).toBe(StartModeEnum.auto);
57
+ });
58
+
59
+ it("reads the start mode from the render options", () => {
60
+ expect(
61
+ resolveStartMode(
62
+ renderOptions({ startMode: StartModeEnum.manual }),
63
+ makeElement(),
64
+ ),
65
+ ).toBe(StartModeEnum.manual);
66
+ });
67
+
68
+ it("reads the start mode from the data attribute", () => {
69
+ expect(
70
+ resolveStartMode(
71
+ renderOptions(),
72
+ makeElement({ [START_MODE_ATTRIBUTE]: "manual" }),
73
+ ),
74
+ ).toBe(StartModeEnum.manual);
75
+ });
76
+
77
+ it("prefers the render options over the attribute", () => {
78
+ expect(
79
+ resolveStartMode(
80
+ renderOptions({ startMode: StartModeEnum.auto }),
81
+ makeElement({ [START_MODE_ATTRIBUTE]: "manual" }),
82
+ ),
83
+ ).toBe(StartModeEnum.auto);
84
+ });
85
+
86
+ it("falls back to auto on an unknown value and says so", () => {
87
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
88
+
89
+ expect(
90
+ resolveStartMode(
91
+ renderOptions(),
92
+ makeElement({ [START_MODE_ATTRIBUTE]: "later" }),
93
+ ),
94
+ ).toBe(StartModeEnum.auto);
95
+ expect(error).toHaveBeenCalledTimes(1);
96
+ expect(error.mock.calls[0]?.[0]).toContain("later");
97
+ });
98
+ });
99
+
100
+ describe("setStartMode", () => {
101
+ it("writes the resolved mode onto the config", () => {
102
+ const config = makeConfig();
103
+ expect(config.startMode).toBe(StartModeEnum.auto);
104
+
105
+ setStartMode(
106
+ renderOptions(),
107
+ makeElement({ [START_MODE_ATTRIBUTE]: "manual" }),
108
+ config,
109
+ );
110
+
111
+ expect(config.startMode).toBe(StartModeEnum.manual);
112
+ });
113
+ });
@@ -15,19 +15,34 @@
15
15
  import createCache, { type EmotionCache } from "@emotion/cache";
16
16
  import { CacheProvider } from "@emotion/react";
17
17
  import type { Ti18n } from "@prosopo/locale";
18
- import type {
19
- Callbacks,
20
- ProcaptchaClientConfigOutput,
21
- ProcaptchaRenderOptions,
18
+ import {
19
+ type Callbacks,
20
+ Placement,
21
+ type PlacementType,
22
+ type ProcaptchaClientConfigOutput,
23
+ type ProcaptchaRenderOptions,
22
24
  } from "@prosopo/types";
23
25
  import type { ReactNode } from "react";
24
26
  import { type Root, createRoot } from "react-dom/client";
25
27
  import { setClientSessionId } from "../clientSession.js";
26
28
  import { createConfig } from "../configCreator.js";
27
29
  import { setLanguage } from "../language.js";
30
+ import { setStartMode } from "../startMode.js";
28
31
  import { setValidChallengeLength } from "../timeout.js";
29
32
  import { BundleCaptcha } from "./components/bundleCaptcha.js";
30
33
 
34
+ const resolveRequestedPlacement = (
35
+ element: Element,
36
+ renderOptions: ProcaptchaRenderOptions,
37
+ ): PlacementType | undefined => {
38
+ const requested =
39
+ renderOptions.placement ?? element.getAttribute("data-placement");
40
+ if (!requested) return undefined;
41
+
42
+ const parsed = Placement.safeParse(requested);
43
+ return parsed.success ? parsed.data : undefined;
44
+ };
45
+
31
46
  interface RenderSettings {
32
47
  identifierPrefix: string;
33
48
  emotionCacheKey: string;
@@ -46,16 +61,20 @@ class CaptchaRenderer {
46
61
  widgetContainer: HTMLElement,
47
62
  sourceElement?: Element,
48
63
  ): Root {
49
- const config = createConfig(
50
- renderOptions.siteKey,
51
- renderOptions.theme,
52
- renderOptions.language,
53
- isWeb2,
64
+ const config = createConfig({
65
+ siteKey: renderOptions.siteKey,
66
+ theme: renderOptions.theme,
67
+ language: renderOptions.language,
68
+ web2: isWeb2,
54
69
  invisible,
55
- renderOptions.userAccountAddress,
56
- renderOptions.ipv4,
57
- renderOptions.ipv6,
58
- );
70
+ placement: resolveRequestedPlacement(
71
+ sourceElement || container,
72
+ renderOptions,
73
+ ),
74
+ userAccountAddress: renderOptions.userAccountAddress,
75
+ ipv4: renderOptions.ipv4,
76
+ ipv6: renderOptions.ipv6,
77
+ });
59
78
  this.readAndValidateSettings(
60
79
  sourceElement || container,
61
80
  config,
@@ -94,6 +113,7 @@ class CaptchaRenderer {
94
113
  setValidChallengeLength(renderOptions, element, config);
95
114
  setLanguage(renderOptions, element, config);
96
115
  setClientSessionId(renderOptions, element, config);
116
+ setStartMode(renderOptions, element, config);
97
117
  }
98
118
 
99
119
  protected makeEmotionCache(
@@ -15,23 +15,39 @@
15
15
  import type { Languages } from "@prosopo/locale";
16
16
  import {
17
17
  EnvironmentTypesSchema,
18
+ type PlacementType,
18
19
  type ProcaptchaClientConfigOutput,
19
20
  ProcaptchaConfigSchema,
21
+ resolvePlacement,
20
22
  } from "@prosopo/types";
21
23
 
24
+ interface CreateConfigOptions {
25
+ siteKey?: string;
26
+ theme?: "light" | "dark";
27
+ language?: (typeof Languages)[keyof typeof Languages];
28
+ web2?: boolean;
29
+ invisible?: boolean;
30
+ placement?: PlacementType;
31
+ userAccountAddress?: string;
32
+ ipv4?: boolean;
33
+ ipv6?: boolean;
34
+ }
35
+
22
36
  function createConfig(
23
- siteKey?: string,
24
- theme: "light" | "dark" = "light",
25
- language?: (typeof Languages)[keyof typeof Languages],
26
- web2 = true,
27
- invisible = false,
28
- userAccountAddress?: string,
29
- ipv4 = false,
30
- ipv6 = false,
37
+ options: CreateConfigOptions = {},
31
38
  ): ProcaptchaClientConfigOutput {
32
- if (!siteKey) {
33
- siteKey = process.env.PROSOPO_SITE_KEY || "";
34
- }
39
+ const {
40
+ theme = "light",
41
+ language,
42
+ web2 = true,
43
+ invisible = false,
44
+ placement,
45
+ userAccountAddress,
46
+ ipv4 = false,
47
+ ipv6 = false,
48
+ } = options;
49
+
50
+ const siteKey = options.siteKey || process.env.PROSOPO_SITE_KEY || "";
35
51
 
36
52
  return ProcaptchaConfigSchema.parse({
37
53
  defaultEnvironment: process.env.PROSOPO_DEFAULT_ENVIRONMENT
@@ -45,6 +61,7 @@ function createConfig(
45
61
  mongoAtlasUri: process.env.PROSOPO_MONGO_EVENTS_URI || "",
46
62
  web2,
47
63
  mode: invisible ? "invisible" : "visible",
64
+ placement: resolvePlacement(placement, invisible),
48
65
  theme,
49
66
  language,
50
67
  ipv4,
@@ -53,3 +70,4 @@ function createConfig(
53
70
  }
54
71
 
55
72
  export { createConfig };
73
+ export type { CreateConfigOptions };
@@ -0,0 +1,53 @@
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 {
16
+ type ProcaptchaClientConfigOutput,
17
+ type ProcaptchaRenderOptions,
18
+ type StartMode,
19
+ StartModeEnum,
20
+ StartModeSchema,
21
+ } from "@prosopo/types";
22
+
23
+ export const START_MODE_ATTRIBUTE = "data-start-mode";
24
+
25
+ export const resolveStartMode = (
26
+ renderOptions: ProcaptchaRenderOptions | undefined,
27
+ element: Element,
28
+ ): StartMode => {
29
+ const requested =
30
+ renderOptions?.startMode || element.getAttribute(START_MODE_ATTRIBUTE);
31
+
32
+ if (!requested) {
33
+ return StartModeEnum.auto;
34
+ }
35
+
36
+ const parsed = StartModeSchema.safeParse(requested);
37
+ if (!parsed.success) {
38
+ console.error(
39
+ `Ignoring unknown start mode "${requested}"; expected one of ${StartModeSchema.options.join(", ")}`,
40
+ );
41
+ return StartModeEnum.auto;
42
+ }
43
+
44
+ return parsed.data;
45
+ };
46
+
47
+ export const setStartMode = (
48
+ renderOptions: ProcaptchaRenderOptions | undefined,
49
+ element: Element,
50
+ config: ProcaptchaClientConfigOutput,
51
+ ): void => {
52
+ config.startMode = resolveStartMode(renderOptions, element);
53
+ };
@@ -29,6 +29,12 @@ import type { CaptchaRenderer } from "./captcha/captchaRenderer.js";
29
29
  import { resolveLanguage } from "./language.js";
30
30
  import type { WidgetThemeResolver } from "./widgetThemeResolver.js";
31
31
 
32
+ /** A mounted widget and the element it listens on for `procaptcha:execute`. */
33
+ interface CreatedWidget {
34
+ root: Root;
35
+ container: HTMLElement;
36
+ }
37
+
32
38
  class WidgetFactory {
33
39
  private captchaRenderer: CaptchaRenderer | null = null;
34
40
  private _i18n: Ti18n | null = null;
@@ -49,7 +55,7 @@ class WidgetFactory {
49
55
  renderOptions: ProcaptchaRenderOptions,
50
56
  isWeb2 = true,
51
57
  invisible = false,
52
- ): Promise<Root[]> {
58
+ ): Promise<CreatedWidget[]> {
53
59
  return Promise.all(
54
60
  containers.map((container) => {
55
61
  const callbacks = getDefaultCallbacks(container);
@@ -71,7 +77,7 @@ class WidgetFactory {
71
77
  callbacks: Callbacks,
72
78
  isWeb2 = true,
73
79
  invisible = false,
74
- ): Promise<Root> {
80
+ ): Promise<CreatedWidget> {
75
81
  renderOptions.theme = this.widgetThemeResolver.resolveWidgetTheme(
76
82
  container,
77
83
  renderOptions,
@@ -127,7 +133,7 @@ class WidgetFactory {
127
133
  container,
128
134
  );
129
135
 
130
- return captchaRoot;
136
+ return { root: captchaRoot, container: widgetContainer };
131
137
  }
132
138
 
133
139
  protected async getCaptchaRenderer(
@@ -158,3 +164,4 @@ class WidgetFactory {
158
164
  }
159
165
 
160
166
  export { WidgetFactory };
167
+ export type { CreatedWidget };