@prosopo/procaptcha-frictionless 2.13.1 → 2.13.3
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 +3 -3
- package/.turbo/turbo-build$colon$tsc.log +24 -24
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +47 -0
- package/dist/cjs/customDetectBot.cjs +14 -12
- package/dist/customDetectBot.d.ts.map +1 -1
- package/dist/customDetectBot.js +14 -12
- package/dist/customDetectBot.js.map +1 -1
- package/dist/tests/customDetectBotAssignTimeout.test.d.ts +2 -0
- package/dist/tests/customDetectBotAssignTimeout.test.d.ts.map +1 -0
- package/dist/tests/customDetectBotAssignTimeout.test.js +118 -0
- package/dist/tests/customDetectBotAssignTimeout.test.js.map +1 -0
- package/package.json +10 -10
- package/src/customDetectBot.ts +36 -25
- package/src/tests/customDetectBotAssignTimeout.test.ts +180 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/customDetectBot.ts
CHANGED
|
@@ -34,11 +34,12 @@ import {
|
|
|
34
34
|
takePrefetchedDetector,
|
|
35
35
|
} from "./detectorPrefetch.js";
|
|
36
36
|
|
|
37
|
-
// Upper bound on the
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
37
|
+
// Upper bound on the assign POST, which serves the detector bundle inline
|
|
38
|
+
// (~215 KB gzipped). The hop is always cold — /healthz pins a different
|
|
39
|
+
// hostname, so it pays fresh DNS + TLS + a CORS preflight before the download
|
|
40
|
+
// even starts, measured at 6.7s against staging. Anything tighter silently
|
|
41
|
+
// drops the detector and sends an empty token.
|
|
42
|
+
const ASSIGN_TIMEOUT_MS = 10_000;
|
|
42
43
|
|
|
43
44
|
// The page(s) the widget is rendered on, reduced to origin + path.
|
|
44
45
|
// Deliberately built from `origin` + `pathname` (never `href`) so the query
|
|
@@ -215,8 +216,7 @@ const customDetectBot: BotDetectionFunction = async (
|
|
|
215
216
|
// in the provider-served pool: when the provider has a populated pool it
|
|
216
217
|
// returns the obfuscated detector (each with its own keys + inner cipher)
|
|
217
218
|
// plus a detectorSessionId. When it cannot (no pool, network, decode, or
|
|
218
|
-
// timeout) we have NO detector to run
|
|
219
|
-
// signal `detectorUnavailable` and the provider serves a PoW challenge.
|
|
219
|
+
// timeout) we have NO detector to run and there is no bundled fallback.
|
|
220
220
|
let detectorSessionId: string | undefined;
|
|
221
221
|
let providerDetect: DetectorType | undefined;
|
|
222
222
|
try {
|
|
@@ -229,27 +229,40 @@ const customDetectBot: BotDetectionFunction = async (
|
|
|
229
229
|
ASSIGN_TIMEOUT_MS,
|
|
230
230
|
));
|
|
231
231
|
if (assigned.useProviderBundle && assigned.detectorScript) {
|
|
232
|
+
// Deliberately untimed: this is a local parse + evaluate, so a timer
|
|
233
|
+
// cannot rescue a stalled main thread — it can only throw away a bundle
|
|
234
|
+
// we are already holding.
|
|
235
|
+
providerDetect = await DetectorLoaderFromScript(assigned.detectorScript);
|
|
232
236
|
detectorSessionId = assigned.detectorSessionId;
|
|
233
|
-
providerDetect = await withTimeout(
|
|
234
|
-
DetectorLoaderFromScript(assigned.detectorScript),
|
|
235
|
-
ASSIGN_TIMEOUT_MS,
|
|
236
|
-
);
|
|
237
237
|
}
|
|
238
|
-
} catch {
|
|
238
|
+
} catch (err) {
|
|
239
239
|
// No detector available — fall through to the PoW request below.
|
|
240
|
+
//
|
|
241
|
+
// Report it. Falling back is by design, but the reasons are not equal: a
|
|
242
|
+
// slow network is routine, whereas a bundle that throws on import means
|
|
243
|
+
// every session on this provider silently degrades to an image captcha
|
|
244
|
+
// with nothing in the client or provider logs to say why. That failure
|
|
245
|
+
// mode shipped undetected once already — an obfuscator seed emitted a
|
|
246
|
+
// bundle that died with "Class constructor X cannot be invoked without
|
|
247
|
+
// 'new'", and the only way to see it was to reproduce the import by hand.
|
|
248
|
+
// The catch stays broad; it just no longer hides what it caught.
|
|
249
|
+
console.error(
|
|
250
|
+
"Procaptcha: no detector bundle available, falling back to a server-chosen captcha:",
|
|
251
|
+
err,
|
|
252
|
+
);
|
|
240
253
|
}
|
|
241
254
|
|
|
242
255
|
const ExtClass = await extClassPromise;
|
|
243
256
|
const ext = new ExtClass();
|
|
244
257
|
|
|
245
|
-
// No provider detector ⇒ no detection is possible
|
|
246
|
-
//
|
|
247
|
-
//
|
|
258
|
+
// No provider detector ⇒ no detection is possible, so there is nothing to
|
|
259
|
+
// send. The request goes out with an empty token and the provider decides
|
|
260
|
+
// what to serve; the client gets no say in that.
|
|
248
261
|
if (providerDetect === undefined) {
|
|
249
262
|
const userAccount = await ext.getAccount(config);
|
|
250
263
|
const { currentUrl: fallbackUrl, iframeUrl: fallbackIframeUrl } =
|
|
251
264
|
getCurrentPageUrls();
|
|
252
|
-
const
|
|
265
|
+
const captcha = await withTimeout(
|
|
253
266
|
providerApi.getFrictionlessCaptcha(
|
|
254
267
|
undefined,
|
|
255
268
|
undefined,
|
|
@@ -258,15 +271,14 @@ const customDetectBot: BotDetectionFunction = async (
|
|
|
258
271
|
config.mode,
|
|
259
272
|
undefined,
|
|
260
273
|
undefined,
|
|
261
|
-
true,
|
|
262
274
|
fallbackUrl,
|
|
263
275
|
fallbackIframeUrl,
|
|
264
276
|
),
|
|
265
277
|
10000,
|
|
266
278
|
);
|
|
267
|
-
if (
|
|
279
|
+
if (captcha.dns_url) {
|
|
268
280
|
try {
|
|
269
|
-
void fetch(
|
|
281
|
+
void fetch(captcha.dns_url, {
|
|
270
282
|
method: "GET",
|
|
271
283
|
mode: "no-cors",
|
|
272
284
|
credentials: "omit",
|
|
@@ -278,13 +290,13 @@ const customDetectBot: BotDetectionFunction = async (
|
|
|
278
290
|
}
|
|
279
291
|
}
|
|
280
292
|
return {
|
|
281
|
-
captchaType:
|
|
282
|
-
sessionId:
|
|
293
|
+
captchaType: captcha.captchaType,
|
|
294
|
+
sessionId: captcha.sessionId,
|
|
283
295
|
provider: provider,
|
|
284
|
-
status:
|
|
296
|
+
status: captcha.status,
|
|
285
297
|
userAccount: userAccount,
|
|
286
|
-
error:
|
|
287
|
-
hp:
|
|
298
|
+
error: captcha.error,
|
|
299
|
+
hp: captcha.hp,
|
|
288
300
|
};
|
|
289
301
|
}
|
|
290
302
|
|
|
@@ -311,7 +323,6 @@ const customDetectBot: BotDetectionFunction = async (
|
|
|
311
323
|
config.mode,
|
|
312
324
|
undefined,
|
|
313
325
|
detectorSessionId,
|
|
314
|
-
undefined,
|
|
315
326
|
currentUrl,
|
|
316
327
|
iframeUrl,
|
|
317
328
|
);
|
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
/**
|
|
16
|
+
* A slow detector assignment must still be used. Under the old 2000 ms cap on
|
|
17
|
+
* both the assign POST and the blob import, a cold-connection assign (measured
|
|
18
|
+
* at 6.7s against staging: fresh DNS + TLS + CORS preflight before ~215 KB of
|
|
19
|
+
* bundle) blew the deadline, the surrounding catch swallowed it, and the
|
|
20
|
+
* frictionless POST went out with an empty token and no detectorSessionId.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { AssignDetectorBundleResponse } from "@prosopo/types";
|
|
24
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
25
|
+
|
|
26
|
+
// `vi.mock` is hoisted; share state via `vi.hoisted` so the factories can
|
|
27
|
+
// reference the same spies the tests assert against.
|
|
28
|
+
const mocks = vi.hoisted(() => ({
|
|
29
|
+
getFrictionlessCaptcha: vi.fn(),
|
|
30
|
+
getProcaptchaRandomActiveProvider: vi.fn(),
|
|
31
|
+
assignDetectorBundle: vi.fn(),
|
|
32
|
+
detectorLoaderFromScript: vi.fn(),
|
|
33
|
+
detect: vi.fn(),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock("@prosopo/api", () => ({
|
|
37
|
+
// customDetectBot does `new ProviderApi(...)`, so the implementation has to
|
|
38
|
+
// be constructible — an arrow function has no [[Construct]] slot.
|
|
39
|
+
ProviderApi: vi.fn(function () {
|
|
40
|
+
return {
|
|
41
|
+
getFrictionlessCaptcha: mocks.getFrictionlessCaptcha,
|
|
42
|
+
assignDetectorBundle: mocks.assignDetectorBundle,
|
|
43
|
+
};
|
|
44
|
+
}),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
vi.mock("@prosopo/procaptcha-common", () => ({
|
|
48
|
+
ExtensionLoader: vi.fn(async () => {
|
|
49
|
+
return class FakeExtension {
|
|
50
|
+
getAccount() {
|
|
51
|
+
return Promise.resolve({
|
|
52
|
+
account: { address: "5FakeUserAccountAddress" },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}),
|
|
57
|
+
getProcaptchaRandomActiveProvider: mocks.getProcaptchaRandomActiveProvider,
|
|
58
|
+
pickIpMode: vi.fn(() => undefined),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
vi.mock("../detectorLoader.js", () => ({
|
|
62
|
+
DetectorLoaderFromScript: mocks.detectorLoaderFromScript,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
import customDetectBot from "../customDetectBot.js";
|
|
66
|
+
|
|
67
|
+
const baseConfig = {
|
|
68
|
+
account: { address: "5CcNvLUdiXFpzKDMjThGLSK9rhWHA1H4EF3zrgkpkjAdqmuP" },
|
|
69
|
+
defaultEnvironment: "staging" as const,
|
|
70
|
+
web2: true,
|
|
71
|
+
mode: "visible" as const,
|
|
72
|
+
} as unknown as Parameters<typeof customDetectBot>[0];
|
|
73
|
+
|
|
74
|
+
const assignResponse: AssignDetectorBundleResponse = {
|
|
75
|
+
useProviderBundle: true,
|
|
76
|
+
detectorSessionId: "det-5cad2419-43c0-41b7-a9b8-0117767624bc",
|
|
77
|
+
detectorScript: "SELF_CONTAINED_ESM",
|
|
78
|
+
status: "ok",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const detectionResult = {
|
|
82
|
+
token: "ENCRYPTED_TOKEN",
|
|
83
|
+
encryptHeadHash: "ENCRYPTED_HEAD_HASH",
|
|
84
|
+
userAccount: { account: { address: "5FakeUserAccountAddress" } },
|
|
85
|
+
shadowDomCleanup: () => undefined,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const captchaResponse = {
|
|
89
|
+
captchaType: "pow",
|
|
90
|
+
sessionId: "SID",
|
|
91
|
+
status: "ok",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// The measured cold-connection assign against staging.
|
|
95
|
+
const SLOW_MS = 6700;
|
|
96
|
+
|
|
97
|
+
const resolveAfter = <T>(ms: number, value: T): Promise<T> =>
|
|
98
|
+
new Promise<T>((resolve) => {
|
|
99
|
+
setTimeout(() => resolve(value), ms);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// The token the frictionless POST was actually called with (first positional
|
|
103
|
+
// arg of `getFrictionlessCaptcha`).
|
|
104
|
+
const postedToken = (): string | undefined => {
|
|
105
|
+
const call = mocks.getFrictionlessCaptcha.mock.calls[0];
|
|
106
|
+
return call?.[0] as string | undefined;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const postedDetectorSessionId = (): string | undefined => {
|
|
110
|
+
const call = mocks.getFrictionlessCaptcha.mock.calls[0];
|
|
111
|
+
return call?.[6] as string | undefined;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
vi.useFakeTimers();
|
|
116
|
+
mocks.getFrictionlessCaptcha.mockReset();
|
|
117
|
+
mocks.getProcaptchaRandomActiveProvider.mockReset();
|
|
118
|
+
mocks.assignDetectorBundle.mockReset();
|
|
119
|
+
mocks.detectorLoaderFromScript.mockReset();
|
|
120
|
+
mocks.detect.mockReset();
|
|
121
|
+
|
|
122
|
+
mocks.getProcaptchaRandomActiveProvider.mockResolvedValue({
|
|
123
|
+
providerAccount: "dns-routed",
|
|
124
|
+
provider: { url: "https://staging-pronode3.prosopo.io" },
|
|
125
|
+
});
|
|
126
|
+
mocks.detect.mockResolvedValue(detectionResult);
|
|
127
|
+
mocks.detectorLoaderFromScript.mockResolvedValue(mocks.detect);
|
|
128
|
+
mocks.assignDetectorBundle.mockResolvedValue(assignResponse);
|
|
129
|
+
mocks.getFrictionlessCaptcha.mockResolvedValue(captchaResponse);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
afterEach(() => {
|
|
133
|
+
vi.useRealTimers();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const runDetection = async (): Promise<void> => {
|
|
137
|
+
const pending = customDetectBot(baseConfig, undefined, () => undefined);
|
|
138
|
+
await vi.advanceTimersByTimeAsync(SLOW_MS + 1000);
|
|
139
|
+
await pending;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
describe("customDetectBot detector-assignment deadline", () => {
|
|
143
|
+
it("uses a bundle whose assign POST is slow", async () => {
|
|
144
|
+
mocks.assignDetectorBundle.mockImplementation(() =>
|
|
145
|
+
resolveAfter(SLOW_MS, assignResponse),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await runDetection();
|
|
149
|
+
|
|
150
|
+
expect(mocks.detectorLoaderFromScript).toHaveBeenCalledWith(
|
|
151
|
+
assignResponse.detectorScript,
|
|
152
|
+
);
|
|
153
|
+
expect(postedToken()).toBe("ENCRYPTED_TOKEN");
|
|
154
|
+
expect(postedDetectorSessionId()).toBe(assignResponse.detectorSessionId);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("uses a bundle whose blob import is slow", async () => {
|
|
158
|
+
mocks.detectorLoaderFromScript.mockImplementation(() =>
|
|
159
|
+
resolveAfter(SLOW_MS, mocks.detect),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
await runDetection();
|
|
163
|
+
|
|
164
|
+
expect(postedToken()).toBe("ENCRYPTED_TOKEN");
|
|
165
|
+
expect(postedDetectorSessionId()).toBe(assignResponse.detectorSessionId);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("still sends an empty token when the provider has no bundle to assign", async () => {
|
|
169
|
+
mocks.assignDetectorBundle.mockResolvedValue({
|
|
170
|
+
useProviderBundle: false,
|
|
171
|
+
status: "ok",
|
|
172
|
+
} satisfies AssignDetectorBundleResponse);
|
|
173
|
+
|
|
174
|
+
await runDetection();
|
|
175
|
+
|
|
176
|
+
expect(mocks.detectorLoaderFromScript).not.toHaveBeenCalled();
|
|
177
|
+
expect(postedToken()).toBeUndefined();
|
|
178
|
+
expect(postedDetectorSessionId()).toBeUndefined();
|
|
179
|
+
});
|
|
180
|
+
});
|