@prosopo/procaptcha-frictionless 2.16.3 → 2.16.4

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.
@@ -0,0 +1,152 @@
1
+ import { CaptchaType, ModeEnum, StartModeEnum, } from "@prosopo/types";
2
+ import { act, createElement, useEffect } 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 EXECUTE_EVENT = "procaptcha:execute";
7
+ const mocks = vi.hoisted(() => ({
8
+ executes: [],
9
+ }));
10
+ const ListeningWidget = (props) => {
11
+ const { container } = props;
12
+ const invisible = props.config.mode === "invisible";
13
+ useEffect(() => {
14
+ const onContainer = () => mocks.executes.push("container");
15
+ const onDocument = () => mocks.executes.push("document");
16
+ container?.addEventListener(EXECUTE_EVENT, onContainer);
17
+ if (invisible)
18
+ document.addEventListener(EXECUTE_EVENT, onDocument);
19
+ return () => {
20
+ container?.removeEventListener(EXECUTE_EVENT, onContainer);
21
+ if (invisible)
22
+ document.removeEventListener(EXECUTE_EVENT, onDocument);
23
+ };
24
+ }, [container, invisible]);
25
+ return createElement("div", { "data-widget": "image" });
26
+ };
27
+ vi.mock("@prosopo/procaptcha-react", () => ({ Procaptcha: ListeningWidget }));
28
+ vi.mock("@prosopo/procaptcha-pow", () => ({ ProcaptchaPow: ListeningWidget }));
29
+ vi.mock("@prosopo/procaptcha-puzzle", () => ({
30
+ ProcaptchaPuzzle: ListeningWidget,
31
+ }));
32
+ vi.mock("@prosopo/procaptcha-common", async (importOriginal) => {
33
+ const actual = await importOriginal();
34
+ return { ...actual, isSecureBrowserContext: () => true };
35
+ });
36
+ const { ProcaptchaFrictionless } = await import("../ProcaptchaFrictionless.js");
37
+ const config = (overrides = {}) => ({
38
+ account: { address: "5siteKey" },
39
+ userAccountAddress: "",
40
+ web2: true,
41
+ mode: ModeEnum.invisible,
42
+ startMode: StartModeEnum.auto,
43
+ ...overrides,
44
+ });
45
+ const i18nStub = {
46
+ isInitialized: true,
47
+ language: "en",
48
+ t: (key) => key,
49
+ changeLanguage: vi.fn(),
50
+ };
51
+ const detectionResult = () => ({
52
+ status: "ok",
53
+ captchaType: CaptchaType.image,
54
+ sessionId: "provider-session",
55
+ provider: { provider: { url: "https://provider.test" } },
56
+ userAccount: { account: { address: "5FakeUserAccountAddress" } },
57
+ });
58
+ const heldDetection = () => {
59
+ let resolve;
60
+ const detectBot = vi.fn().mockImplementation(() => new Promise((r) => {
61
+ resolve = r;
62
+ }));
63
+ const finish = async () => {
64
+ await act(async () => {
65
+ resolve?.(detectionResult());
66
+ });
67
+ };
68
+ return { detectBot, finish };
69
+ };
70
+ let host;
71
+ let container;
72
+ let root;
73
+ const mountWrapper = async (detectBot, overrides = {}) => {
74
+ await act(async () => {
75
+ root.render(createElement(ProcaptchaFrictionless, {
76
+ config: config(overrides),
77
+ callbacks: {},
78
+ restart: vi.fn(),
79
+ i18n: i18nStub,
80
+ detectBot,
81
+ container,
82
+ }));
83
+ });
84
+ };
85
+ const dispatch = async (target) => {
86
+ await act(async () => {
87
+ target.dispatchEvent(new CustomEvent(EXECUTE_EVENT));
88
+ });
89
+ };
90
+ beforeEach(() => {
91
+ mocks.executes.length = 0;
92
+ host = document.createElement("div");
93
+ container = document.createElement("div");
94
+ document.body.append(host, container);
95
+ act(() => {
96
+ root = createRoot(host);
97
+ });
98
+ });
99
+ afterEach(() => {
100
+ act(() => {
101
+ root.unmount();
102
+ });
103
+ host.remove();
104
+ container.remove();
105
+ vi.clearAllMocks();
106
+ });
107
+ describe("execute() before the inner widget is listening", () => {
108
+ it("replays a bare execute() once the invisible widget mounts", async () => {
109
+ const { detectBot, finish } = heldDetection();
110
+ await mountWrapper(detectBot);
111
+ await dispatch(document);
112
+ expect(mocks.executes).toEqual([]);
113
+ await finish();
114
+ expect(mocks.executes).toEqual(["container"]);
115
+ });
116
+ it("replays a targeted execute() in visible mode", async () => {
117
+ const { detectBot, finish } = heldDetection();
118
+ await mountWrapper(detectBot, { mode: ModeEnum.visible });
119
+ await dispatch(container);
120
+ await finish();
121
+ expect(mocks.executes).toEqual(["container"]);
122
+ });
123
+ it("replays repeated early calls only once", async () => {
124
+ const { detectBot, finish } = heldDetection();
125
+ await mountWrapper(detectBot);
126
+ await dispatch(document);
127
+ await dispatch(document);
128
+ await finish();
129
+ expect(mocks.executes).toEqual(["container"]);
130
+ });
131
+ it("does not start the widget when execute() was never called", async () => {
132
+ const { detectBot, finish } = heldDetection();
133
+ await mountWrapper(detectBot);
134
+ await finish();
135
+ expect(mocks.executes).toEqual([]);
136
+ });
137
+ it("leaves an execute() after mount to the widget alone", async () => {
138
+ const { detectBot, finish } = heldDetection();
139
+ await mountWrapper(detectBot);
140
+ await finish();
141
+ await dispatch(document);
142
+ expect(mocks.executes).toEqual(["document"]);
143
+ });
144
+ it("ignores a bare execute() in visible mode, as the widgets do", async () => {
145
+ const { detectBot, finish } = heldDetection();
146
+ await mountWrapper(detectBot, { mode: ModeEnum.visible });
147
+ await dispatch(document);
148
+ await finish();
149
+ expect(mocks.executes).toEqual([]);
150
+ });
151
+ });
152
+ //# sourceMappingURL=executeBeforeMount.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executeBeforeMount.test.js","sourceRoot":"","sources":["../../src/tests/executeBeforeMount.test.tsx"],"names":[],"mappings":"AAeA,OAAO,EAIN,WAAW,EACX,QAAQ,EAIR,aAAa,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAqB,GAAG,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACzE,OAAO,EAAa,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAMzE,UAAU,CAAC,wBAAwB,GAAG,IAAI,CAAC;AAE3C,MAAM,aAAa,GAAG,oBAAoB,CAAC;AAE3C,MAAM,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC/B,QAAQ,EAAE,EAAkC;CAC5C,CAAC,CAAC,CAAC;AAIJ,MAAM,eAAe,GAAG,CAAC,KAAsB,EAAE,EAAE;IAClD,MAAM,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IAC5B,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,WAAW,CAAC;IACpD,SAAS,CAAC,GAAG,EAAE;QACd,MAAM,WAAW,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC3D,MAAM,UAAU,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACzD,SAAS,EAAE,gBAAgB,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;QACxD,IAAI,SAAS;YAAE,QAAQ,CAAC,gBAAgB,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;QACpE,OAAO,GAAG,EAAE;YACX,SAAS,EAAE,mBAAmB,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;YAC3D,IAAI,SAAS;gBAAE,QAAQ,CAAC,mBAAmB,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;QACxE,CAAC,CAAC;IACH,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC;IAC3B,OAAO,aAAa,CAAC,KAAK,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC;AACzD,CAAC,CAAC;AAEF,EAAE,CAAC,IAAI,CAAC,2BAA2B,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC;AAC9E,EAAE,CAAC,IAAI,CAAC,yBAAyB,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,aAAa,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC;AAC/E,EAAE,CAAC,IAAI,CAAC,4BAA4B,EAAE,GAAG,EAAE,CAAC,CAAC;IAC5C,gBAAgB,EAAE,eAAe;CACjC,CAAC,CAAC,CAAC;AAEJ,EAAE,CAAC,IAAI,CAAC,4BAA4B,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE;IAC9D,MAAM,MAAM,GACX,MAAM,cAAc,EAA+C,CAAC;IACrE,OAAO,EAAE,GAAG,MAAM,EAAE,sBAAsB,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC;AAC1D,CAAC,CAAC,CAAC;AAEH,MAAM,EAAE,sBAAsB,EAAE,GAAG,MAAM,MAAM,CAAC,8BAA8B,CAAC,CAAC;AAEhF,MAAM,MAAM,GAAG,CACd,YAAkD,EAAE,EACtB,EAAE,CAAC,CAAC;IAClC,OAAO,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE;IAChC,kBAAkB,EAAE,EAAE;IACtB,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,QAAQ,CAAC,SAAS;IACxB,SAAS,EAAE,aAAa,CAAC,IAAI;IAC7B,GAAG,SAAS;CACZ,CAAC,CAAC;AAEH,MAAM,QAAQ,GAAG;IAChB,aAAa,EAAE,IAAI;IACnB,QAAQ,EAAE,IAAI;IACd,CAAC,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG;IACvB,cAAc,EAAE,EAAE,CAAC,EAAE,EAAE;CACH,CAAC;AAEtB,MAAM,eAAe,GAAG,GAA+B,EAAE,CAAC,CAAC;IAC1D,MAAM,EAAE,IAAI;IACZ,WAAW,EAAE,WAAW,CAAC,KAAK;IAC9B,SAAS,EAAE,kBAAkB;IAC7B,QAAQ,EAAE,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,uBAAuB,EAAE,EAAoB;IAC1E,WAAW,EAAE,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,yBAAyB,EAAE,EAAa;CAC3E,CAAC,CAAC;AAOH,MAAM,aAAa,GAAG,GAAkB,EAAE;IACzC,IAAI,OAAmE,CAAC;IACxE,MAAM,SAAS,GAAG,EAAE,CAAC,EAAE,EAAwB,CAAC,kBAAkB,CACjE,GAAG,EAAE,CACJ,IAAI,OAAO,CAA6B,CAAC,CAAC,EAAE,EAAE;QAC7C,OAAO,GAAG,CAAC,CAAC;IACb,CAAC,CAAC,CACH,CAAC;IACF,MAAM,MAAM,GAAG,KAAK,IAAI,EAAE;QACzB,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;YACpB,OAAO,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC;IACF,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;AAC9B,CAAC,CAAC;AAEF,IAAI,IAAoB,CAAC;AACzB,IAAI,SAAyB,CAAC;AAC9B,IAAI,IAAU,CAAC;AAEf,MAAM,YAAY,GAAG,KAAK,EACzB,SAA+B,EAC/B,YAAkD,EAAE,EACpC,EAAE;IAClB,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;QACpB,IAAI,CAAC,MAAM,CACV,aAAa,CAAC,sBAAsB,EAAE;YACrC,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC;YACzB,SAAS,EAAE,EAAE;YACb,OAAO,EAAE,EAAE,CAAC,EAAE,EAAE;YAChB,IAAI,EAAE,QAAQ;YACd,SAAS;YACT,SAAS;SACT,CAAiB,CAClB,CAAC;IACH,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAmB,EAAiB,EAAE;IAC7D,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;QACpB,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,aAAa,CAAC,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC;AAEF,UAAU,CAAC,GAAG,EAAE;IACf,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IAC1B,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IACrC,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAC1C,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACtC,GAAG,CAAC,GAAG,EAAE;QACR,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,SAAS,CAAC,GAAG,EAAE;IACd,GAAG,CAAC,GAAG,EAAE;QACR,IAAI,CAAC,OAAO,EAAE,CAAC;IAChB,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,MAAM,EAAE,CAAC;IACd,SAAS,CAAC,MAAM,EAAE,CAAC;IACnB,EAAE,CAAC,aAAa,EAAE,CAAC;AACpB,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,gDAAgD,EAAE,GAAG,EAAE;IAC/D,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QAC1E,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,aAAa,EAAE,CAAC;QAC9C,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC;QAE9B,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACzB,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAEnC,MAAM,MAAM,EAAE,CAAC;QAEf,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC7D,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,aAAa,EAAE,CAAC;QAC9C,MAAM,YAAY,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;QAE1D,MAAM,QAAQ,CAAC,SAAS,CAAC,CAAC;QAC1B,MAAM,MAAM,EAAE,CAAC;QAEf,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,aAAa,EAAE,CAAC;QAC9C,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC;QAE9B,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACzB,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACzB,MAAM,MAAM,EAAE,CAAC;QAEf,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QAC1E,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,aAAa,EAAE,CAAC;QAC9C,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC;QAE9B,MAAM,MAAM,EAAE,CAAC;QAEf,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;QACpE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,aAAa,EAAE,CAAC;QAC9C,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC;QAC9B,MAAM,MAAM,EAAE,CAAC;QAEf,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAEzB,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC5E,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,aAAa,EAAE,CAAC;QAC9C,MAAM,YAAY,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;QAE1D,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACzB,MAAM,MAAM,EAAE,CAAC;QAEf,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}
@@ -16,7 +16,12 @@ describe("evaluateFrictionlessResult", () => {
16
16
  expect(evaluateFrictionlessResult({
17
17
  captchaType: "pow",
18
18
  error: { message: "Boom", key: "API.SOMETHING" },
19
- })).toEqual({ kind: "error", message: "Boom", key: "API.SOMETHING" });
19
+ })).toEqual({
20
+ kind: "error",
21
+ message: "Boom",
22
+ key: "API.SOMETHING",
23
+ retryable: true,
24
+ });
20
25
  });
21
26
  it("omits key from the outcome when the server error carries no key", () => {
22
27
  const outcome = evaluateFrictionlessResult({
@@ -32,12 +37,17 @@ describe("evaluateFrictionlessResult", () => {
32
37
  it("halts when captchaType is missing (bare-string 401 body shape from access-policy / autoBan / middleware)", () => {
33
38
  expect(evaluateFrictionlessResult({
34
39
  error: undefined,
35
- })).toEqual({ kind: "error", message: MISSING_CAPTCHA_TYPE_MESSAGE });
40
+ })).toEqual({
41
+ kind: "error",
42
+ message: MISSING_CAPTCHA_TYPE_MESSAGE,
43
+ retryable: false,
44
+ });
36
45
  });
37
46
  it("halts when both captchaType and error are absent (opaque parse failure)", () => {
38
47
  expect(evaluateFrictionlessResult({})).toEqual({
39
48
  kind: "error",
40
49
  message: MISSING_CAPTCHA_TYPE_MESSAGE,
50
+ retryable: false,
41
51
  });
42
52
  });
43
53
  it("prefers the structured error over the missing-captchaType message when both apply", () => {
@@ -47,6 +57,53 @@ describe("evaluateFrictionlessResult", () => {
47
57
  kind: "error",
48
58
  message: "Server error",
49
59
  key: "API.SERVER_ERROR",
60
+ retryable: true,
61
+ });
62
+ });
63
+ describe("retryable classification", () => {
64
+ const retryableOf = (key) => {
65
+ const outcome = evaluateFrictionlessResult({
66
+ error: { message: "msg", ...(key !== undefined && { key }) },
67
+ });
68
+ if (outcome.kind !== "error")
69
+ throw new Error("expected error outcome");
70
+ return outcome.retryable;
71
+ };
72
+ it.each([
73
+ "API.SITE_KEY_NOT_REGISTERED",
74
+ "API.INVALID_SITE_KEY",
75
+ "API.UNAUTHORIZED_ORIGIN_URL",
76
+ "API.INCORRECT_CAPTCHA_TYPE",
77
+ ])("shows integration fault %s rather than re-rolling", (key) => {
78
+ expect(retryableOf(key)).toBe(false);
79
+ });
80
+ it.each([
81
+ "API.ACCESS_POLICY_BLOCK",
82
+ "API.ABUSER_BLOCKED",
83
+ "API.CRAWLER_BLOCKED",
84
+ "API.DATACENTER_BLOCKED",
85
+ "API.DISALLOWED_WEBVIEW",
86
+ "API.MOBILE_BLOCKED",
87
+ "API.PROXY_BLOCKED",
88
+ "API.SATELLITE_BLOCKED",
89
+ "API.TOR_BLOCKED",
90
+ "API.VPN_BLOCKED",
91
+ "API.FORBIDDEN",
92
+ "API.UNAUTHORIZED",
93
+ ])("does not re-roll policy denial %s", (key) => {
94
+ expect(retryableOf(key)).toBe(false);
95
+ });
96
+ it("leaves CAPTCHA.NO_SESSION_FOUND to its own restart timer", () => {
97
+ expect(retryableOf("CAPTCHA.NO_SESSION_FOUND")).toBe(false);
98
+ });
99
+ it("re-rolls API.BAD_REQUEST, which is what an unhandled provider throw becomes", () => {
100
+ expect(retryableOf("API.BAD_REQUEST")).toBe(true);
101
+ });
102
+ it.each(["API.UNKNOWN", "API.INTERNAL_SERVER_ERROR", "DATABASE.UNKNOWN"])("re-rolls unrecognised provider fault %s", (key) => {
103
+ expect(retryableOf(key)).toBe(true);
104
+ });
105
+ it("does not re-roll an error with no key at all", () => {
106
+ expect(retryableOf(undefined)).toBe(false);
50
107
  });
51
108
  });
52
109
  });
@@ -1 +1 @@
1
- {"version":3,"file":"frictionlessResultGuard.test.js","sourceRoot":"","sources":["../../src/tests/frictionlessResultGuard.test.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EACN,4BAA4B,EAC5B,0BAA0B,GAC1B,MAAM,+BAA+B,CAAC;AAWvC,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC3C,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC3D,MAAM,CACL,0BAA0B,CAAC;YAC1B,WAAW,EAAE,KAAK;SAClB,CAAC,CACF,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wEAAwE,EAAE,GAAG,EAAE;QACjF,MAAM,CACL,0BAA0B,CAAC;YAC1B,WAAW,EAAE,OAAO;YACpB,KAAK,EAAE,SAAS;SAChB,CAAC,CACF,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2EAA2E,EAAE,GAAG,EAAE;QACpF,MAAM,CACL,0BAA0B,CAAC;YAC1B,WAAW,EAAE,KAAK;YAClB,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,EAAE;SAChD,CAAC,CACF,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,EAAE,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QAC1E,MAAM,OAAO,GAAG,0BAA0B,CAAC;YAC1C,WAAW,EAAE,KAAK;YAClB,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE;SAC1B,CAAC,CAAC;QACH,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACnC,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;QACxE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,CAAC,KAAK,IAAI,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0GAA0G,EAAE,GAAG,EAAE;QAOnH,MAAM,CACL,0BAA0B,CAAC;YAG1B,KAAK,EAAE,SAAS;SAChB,CAAC,CACF,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,4BAA4B,EAAE,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yEAAyE,EAAE,GAAG,EAAE;QAClF,MAAM,CAAC,0BAA0B,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;YAC9C,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,4BAA4B;SACrC,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mFAAmF,EAAE,GAAG,EAAE;QAG5F,MAAM,CACL,0BAA0B,CAAC;YAC1B,KAAK,EAAE,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,EAAE,kBAAkB,EAAE;SAC3D,CAAC,CACF,CAAC,OAAO,CAAC;YACT,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,cAAc;YACvB,GAAG,EAAE,kBAAkB;SACvB,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"frictionlessResultGuard.test.js","sourceRoot":"","sources":["../../src/tests/frictionlessResultGuard.test.ts"],"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.16.3",
3
+ "version": "2.16.4",
4
4
  "author": "PROSOPO LIMITED <info@prosopo.io>",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",
@@ -30,26 +30,26 @@
30
30
  },
31
31
  "browserslist": ["> 0.5%, last 2 versions, not dead"],
32
32
  "dependencies": {
33
- "@prosopo/api": "4.2.3",
34
- "@prosopo/common": "3.1.55",
35
- "@prosopo/locale": "3.4.2",
36
- "@prosopo/procaptcha-common": "2.14.0",
37
- "@prosopo/procaptcha-pow": "2.12.4",
38
- "@prosopo/procaptcha-puzzle": "2.13.0",
39
- "@prosopo/procaptcha-react": "2.11.0",
40
- "@prosopo/types": "5.8.3",
33
+ "@prosopo/api": "4.2.4",
34
+ "@prosopo/common": "3.1.56",
35
+ "@prosopo/locale": "3.4.3",
36
+ "@prosopo/procaptcha-common": "2.14.1",
37
+ "@prosopo/procaptcha-pow": "2.12.5",
38
+ "@prosopo/procaptcha-puzzle": "2.13.1",
39
+ "@prosopo/procaptcha-react": "2.11.1",
40
+ "@prosopo/types": "5.8.4",
41
41
  "@prosopo/widget-skeleton": "2.8.7",
42
42
  "dotenv": "16.4.5",
43
- "react": "19.2.8"
43
+ "react": "19.3.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@prosopo/config": "3.3.16",
46
+ "@prosopo/config": "3.3.17",
47
47
  "@types/node": "22.10.2",
48
48
  "@vitest/coverage-v8": "4.1.11",
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
+ "react-dom": "19.3.0",
53
53
  "tslib": "2.7.0",
54
54
  "tsx": "4.20.3",
55
55
  "typescript": "5.6.2",
@@ -23,6 +23,7 @@ import {
23
23
  import {
24
24
  CaptchaType,
25
25
  type FrictionlessState,
26
+ ModeEnum,
26
27
  type ModeType,
27
28
  PROCAPTCHA_START_EVENT,
28
29
  ProcaptchaConfigSchema,
@@ -149,6 +150,13 @@ export const ProcaptchaFrictionless = ({
149
150
  const manualStartedRef = useRef(false);
150
151
  // The placeholder is built before `start()` exists in this scope.
151
152
  const manualCheckboxHandlerRef = useRef(noopCheckboxHandler);
153
+ // The inner widget only listens for `procaptcha:execute` once /frictionless
154
+ // has answered, its chunk has loaded and it has mounted. An execute() in
155
+ // that window (e.g. a fast form submit) would otherwise be dropped and the
156
+ // challenge never open, so it is held here and replayed on mount.
157
+ const innerWidgetRenderedRef = useRef(false);
158
+ const innerWidgetListeningRef = useRef(false);
159
+ const pendingExecuteRef = useRef(false);
152
160
 
153
161
  useEffect(() => {
154
162
  if (!config.language) return;
@@ -199,6 +207,7 @@ export const ProcaptchaFrictionless = ({
199
207
  restartComponentTimeout();
200
208
  }, 0);
201
209
  }
210
+ innerWidgetRenderedRef.current = false;
202
211
  setComponentToRender(
203
212
  renderPlaceholder(
204
213
  config.theme,
@@ -321,6 +330,7 @@ export const ProcaptchaFrictionless = ({
321
330
  const startCoords = escalationCoords ?? retryStartCoords;
322
331
  mountCountRef.current += 1;
323
332
  const mountKey = mountCountRef.current;
333
+ innerWidgetRenderedRef.current = true;
324
334
 
325
335
  if (captchaType === CaptchaType.authenticated) {
326
336
  // Web Bot Auth pre-verified pass-through. No challenge, no
@@ -423,6 +433,14 @@ export const ProcaptchaFrictionless = ({
423
433
 
424
434
  const guard = evaluateFrictionlessResult(result);
425
435
  if (guard.kind === "error") {
436
+ // Throwing hands this to providerRetry, which re-rolls onto a
437
+ // different provider. The client does not throw on a 400 with a
438
+ // JSON body, so without this an unrecognised provider-side
439
+ // failure stranded the user on the first response even when
440
+ // every other node was healthy.
441
+ if (guard.retryable) {
442
+ throw new Error(guard.message);
443
+ }
426
444
  stateRef.current = {
427
445
  ...stateRef.current,
428
446
  loading: false,
@@ -461,6 +479,10 @@ export const ProcaptchaFrictionless = ({
461
479
  5,
462
480
  ).finally(() => {
463
481
  if (stateRef.current.attemptCount >= 5) {
482
+ // Retries swallow the underlying error, so without this a site's
483
+ // error callback never fires for a failure that retried — it would
484
+ // have fired immediately before retrying was introduced.
485
+ events.onError(new Error("Cannot load CAPTCHA"));
464
486
  fallOverWithStyle();
465
487
  restartComponentTimeout();
466
488
  }
@@ -475,6 +497,7 @@ export const ProcaptchaFrictionless = ({
475
497
  manualStartedRef.current = true;
476
498
  pendingRetryCoordsRef.current = coords ?? null;
477
499
  nextMountAutoStartRef.current = autoStart;
500
+ innerWidgetRenderedRef.current = false;
478
501
  setComponentToRender(
479
502
  renderPlaceholder(
480
503
  config.theme,
@@ -520,6 +543,40 @@ export const ProcaptchaFrictionless = ({
520
543
  };
521
544
  }, [manualStart, container]);
522
545
 
546
+ // Mirrors the inner widgets' own listeners: a targeted execute() arrives on
547
+ // the container in either mode, a bare one on document in invisible mode.
548
+ // Manual start has its own listener above that defers the whole flow.
549
+ useEffect(() => {
550
+ if (manualStart) return;
551
+ const invisible = config.mode === ModeEnum.invisible;
552
+ const onExecuteEvent = () => {
553
+ if (!innerWidgetListeningRef.current) pendingExecuteRef.current = true;
554
+ };
555
+ container?.addEventListener(PROCAPTCHA_EXECUTE_EVENT, onExecuteEvent);
556
+ if (invisible) {
557
+ document.addEventListener(PROCAPTCHA_EXECUTE_EVENT, onExecuteEvent);
558
+ }
559
+ return () => {
560
+ container?.removeEventListener(PROCAPTCHA_EXECUTE_EVENT, onExecuteEvent);
561
+ if (invisible) {
562
+ document.removeEventListener(PROCAPTCHA_EXECUTE_EVENT, onExecuteEvent);
563
+ }
564
+ };
565
+ }, [manualStart, config.mode, container]);
566
+
567
+ // Child effects run before this one, so a newly mounted inner widget is
568
+ // already listening when the held execute() is replayed. It goes to the
569
+ // container, not document, so other widgets on the page don't run twice.
570
+ // biome-ignore lint/correctness/useExhaustiveDependencies: intentional — must run after every commit that swaps the rendered widget.
571
+ useEffect(() => {
572
+ innerWidgetListeningRef.current = innerWidgetRenderedRef.current;
573
+ if (!innerWidgetListeningRef.current || !pendingExecuteRef.current) return;
574
+ pendingExecuteRef.current = false;
575
+ (container ?? document).dispatchEvent(
576
+ new CustomEvent(PROCAPTCHA_EXECUTE_EVENT),
577
+ );
578
+ }, [componentToRender, container]);
579
+
523
580
  // Track which config identity has already been started for. Host
524
581
  // pages often recreate the `callbacks` object (and sometimes the whole
525
582
  // `config`) on every render, which — before this guard — re-fired
@@ -14,7 +14,41 @@
14
14
 
15
15
  export type FrictionlessGuardOutcome =
16
16
  | { kind: "pass" }
17
- | { kind: "error"; message: string; key?: string };
17
+ | { kind: "error"; message: string; key?: string; retryable: boolean };
18
+
19
+ /**
20
+ * Error keys that describe the caller's situation rather than the provider's
21
+ * health, so re-rolling onto another provider would return the same answer.
22
+ *
23
+ * Two groups. Integration faults (site key, origin, captcha type) need the
24
+ * site owner to change something and the text is what tells them what. Policy
25
+ * denials are a decision about this visitor that every provider shares —
26
+ * retrying them would both mislead the user and hammer the fleet on behalf of
27
+ * traffic we have just refused.
28
+ *
29
+ * Anything outside this list is treated as the provider failing, not the
30
+ * caller: see {@link evaluateFrictionlessResult}.
31
+ */
32
+ const TERMINAL_ERROR_KEYS: ReadonlySet<string> = new Set([
33
+ "API.SITE_KEY_NOT_REGISTERED",
34
+ "API.INVALID_SITE_KEY",
35
+ "API.UNAUTHORIZED_ORIGIN_URL",
36
+ "API.INCORRECT_CAPTCHA_TYPE",
37
+ "API.ACCESS_POLICY_BLOCK",
38
+ "API.ABUSER_BLOCKED",
39
+ "API.CRAWLER_BLOCKED",
40
+ "API.DATACENTER_BLOCKED",
41
+ "API.DISALLOWED_WEBVIEW",
42
+ "API.MOBILE_BLOCKED",
43
+ "API.PROXY_BLOCKED",
44
+ "API.SATELLITE_BLOCKED",
45
+ "API.TOR_BLOCKED",
46
+ "API.VPN_BLOCKED",
47
+ "API.FORBIDDEN",
48
+ "API.UNAUTHORIZED",
49
+ // carries its own ten-second restart in ProcaptchaFrictionless
50
+ "CAPTCHA.NO_SESSION_FOUND",
51
+ ]);
18
52
 
19
53
  // Fields the guard reads off the `/frictionless` result. Kept minimal
20
54
  // (subset of BotDetectionFunctionResult) so the helper can be exercised
@@ -49,14 +83,26 @@ export const evaluateFrictionlessResult = (
49
83
  result: FrictionlessGuardInput,
50
84
  ): FrictionlessGuardOutcome => {
51
85
  if (result.error?.message) {
86
+ const key = result.error.key;
52
87
  return {
53
88
  kind: "error",
54
89
  message: result.error.message,
55
- ...(result.error.key !== undefined && { key: result.error.key }),
90
+ ...(key !== undefined && { key }),
91
+ // A key we do not recognise means the provider failed in a way it
92
+ // has no vocabulary for — API.BAD_REQUEST is what an unhandled throw
93
+ // inside a handler serialises to. That is worth trying another
94
+ // provider for. Retrying is gated on having a key at all: the
95
+ // bare-string errors handled below are hard blocks, and re-rolling
96
+ // those would hammer the fleet for traffic already refused.
97
+ retryable: key !== undefined && !TERMINAL_ERROR_KEYS.has(key),
56
98
  };
57
99
  }
58
100
  if (!result.captchaType) {
59
- return { kind: "error", message: MISSING_CAPTCHA_TYPE_MESSAGE };
101
+ return {
102
+ kind: "error",
103
+ message: MISSING_CAPTCHA_TYPE_MESSAGE,
104
+ retryable: false,
105
+ };
60
106
  }
61
107
  return { kind: "pass" };
62
108
  };