@prosopo/procaptcha-frictionless 2.12.24 → 2.13.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.
- package/.turbo/turbo-build$colon$cjs.log +10 -12
- package/.turbo/turbo-build$colon$tsc.log +24 -27
- package/.turbo/turbo-build.log +11 -13
- package/CHANGELOG.md +28 -0
- package/dist/cjs/customDetectBot.cjs +39 -8
- package/dist/cjs/detectorLoader.cjs +18 -2
- package/dist/customDetectBot.d.ts.map +1 -1
- package/dist/customDetectBot.js +40 -9
- package/dist/customDetectBot.js.map +1 -1
- package/dist/detectorLoader.d.ts +30 -3
- package/dist/detectorLoader.d.ts.map +1 -1
- package/dist/detectorLoader.js +18 -2
- package/dist/detectorLoader.js.map +1 -1
- package/dist/tests/customDetectBotSimd.test.js +10 -1
- package/dist/tests/customDetectBotSimd.test.js.map +1 -1
- package/package.json +9 -10
- package/src/customDetectBot.ts +113 -54
- package/src/detectorLoader.ts +65 -3
- package/src/tests/customDetectBotSimd.test.ts +11 -1
- package/tsconfig.cjs.json +0 -3
- package/tsconfig.json +0 -3
- package/tsconfig.tsbuildinfo +1 -1
package/src/customDetectBot.ts
CHANGED
|
@@ -25,7 +25,16 @@ import type {
|
|
|
25
25
|
ProviderSelectRetryContext,
|
|
26
26
|
} from "@prosopo/types";
|
|
27
27
|
import type { BotDetectionFunctionResult } from "@prosopo/types";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
DetectorLoaderFromScript,
|
|
30
|
+
type DetectorType,
|
|
31
|
+
} from "./detectorLoader.js";
|
|
32
|
+
|
|
33
|
+
// Upper bound on the detector-bundle assignment + load probe. The detector
|
|
34
|
+
// lives ONLY in the provider-served pool bundles, so if the provider is
|
|
35
|
+
// slow/unreachable we abandon the probe and signal `detectorUnavailable`, and
|
|
36
|
+
// the provider serves a PoW challenge instead of running detection.
|
|
37
|
+
const ASSIGN_TIMEOUT_MS = 2000;
|
|
29
38
|
|
|
30
39
|
// The page(s) the widget is rendered on, reduced to origin + path.
|
|
31
40
|
// Deliberately built from `origin` + `pathname` (never `href`) so the query
|
|
@@ -129,69 +138,36 @@ const customDetectBot: BotDetectionFunction = async (
|
|
|
129
138
|
restartFn: () => void,
|
|
130
139
|
retryContext?: ProviderSelectRetryContext,
|
|
131
140
|
): Promise<BotDetectionFunctionResult> => {
|
|
132
|
-
// Kick off the provider-pin resolution (which hits `/healthz` on the
|
|
133
|
-
// load-balancer DNS endpoint) in parallel with the module loaders and
|
|
134
|
-
// the whole `detect()` flow. `getProcaptchaRandomActiveProvider` is
|
|
135
|
-
// memoised via `pinPromiseCache` keyed on `(env, ipMode)`, so the
|
|
136
|
-
// in-flight promise is reused when catcher's internal provider selector
|
|
137
|
-
// and the awaited `getProcaptchaRandomActiveProvider(...)` below reach
|
|
138
|
-
// for it — no duplicate healthz request. Before this ordering change
|
|
139
|
-
// the healthz round-trip was fully serial with `detect()`, adding
|
|
140
|
-
// ~100–200ms to the frictionless critical path on cold DNS/TLS.
|
|
141
|
-
//
|
|
142
|
-
// We warm both cache keys because the catcher-internal selector call
|
|
143
|
-
// (line ~145 below) passes no ipMode, while the awaited call further
|
|
144
|
-
// down passes `pickIpMode(config)`. Same key when the dapp opts into
|
|
145
|
-
// neither ipv4 nor ipv6 (dual stack — the common case); different
|
|
146
|
-
// keys when the dapp is pinned to a single stack. Fire-and-forget on
|
|
147
|
-
// both — the real synchronisation happens at the awaits below.
|
|
148
|
-
const ipMode = pickIpMode(config);
|
|
149
|
-
void getProcaptchaRandomActiveProvider(config.defaultEnvironment).catch(
|
|
150
|
-
() => undefined,
|
|
151
|
-
);
|
|
152
|
-
if (ipMode !== undefined) {
|
|
153
|
-
void getProcaptchaRandomActiveProvider(
|
|
154
|
-
config.defaultEnvironment,
|
|
155
|
-
ipMode,
|
|
156
|
-
retryContext,
|
|
157
|
-
).catch(() => undefined);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const [ExtClass, detect] = await Promise.all([
|
|
161
|
-
ExtensionLoader(config.web2),
|
|
162
|
-
DetectorLoader(),
|
|
163
|
-
]);
|
|
164
|
-
const ext = new ExtClass();
|
|
165
|
-
|
|
166
|
-
// The detector bundle still expects the legacy 5-arg signature
|
|
167
|
-
// `(env, randomProviderSelectorFn, container, restart, accountGenerator)`.
|
|
168
|
-
// Until the bundle is rebuilt without the provider selector, hand it a
|
|
169
|
-
// noop selector that resolves to the static DNS endpoint — the detector
|
|
170
|
-
// no longer uses the returned RandomProvider for routing.
|
|
171
|
-
const detectionResult = await detect(
|
|
172
|
-
config.defaultEnvironment,
|
|
173
|
-
async () => getProcaptchaRandomActiveProvider(config.defaultEnvironment),
|
|
174
|
-
container,
|
|
175
|
-
restartFn,
|
|
176
|
-
() => ext.getAccount(config),
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
const userAccount = detectionResult.userAccount;
|
|
180
|
-
|
|
181
141
|
if (!config.account.address) {
|
|
182
142
|
throw new ProsopoEnvError("GENERAL.SITE_KEY_MISSING");
|
|
183
143
|
}
|
|
184
144
|
|
|
145
|
+
const ipMode = pickIpMode(config);
|
|
146
|
+
|
|
147
|
+
// Start the extension/account module load now rather than after the bundle
|
|
148
|
+
// assignment. It depends on nothing above, so it overlaps the provider-pin
|
|
149
|
+
// `/healthz` round-trip and the assign request for free; it is awaited below
|
|
150
|
+
// at the point `ExtClass` is first needed.
|
|
151
|
+
//
|
|
152
|
+
// This is what survives of main's parallel-pin optimisation. That change
|
|
153
|
+
// warmed BOTH `pinPromiseCache` keys — with and without ipMode — because the
|
|
154
|
+
// catcher-internal provider selector called with no ipMode while the awaited
|
|
155
|
+
// call passed one. The 3-arg detector has no provider selector at all, so
|
|
156
|
+
// only the `(env, ipMode)` key is ever used now and warming the bare-env key
|
|
157
|
+
// would just issue a healthz request nothing consumes. Nor can the pin be
|
|
158
|
+
// moved off the critical path the way it was before: the detector now comes
|
|
159
|
+
// FROM the provider, so resolving the provider is a hard prerequisite of
|
|
160
|
+
// having anything to run, not something that can proceed alongside it.
|
|
161
|
+
const extClassPromise = ExtensionLoader(config.web2);
|
|
162
|
+
|
|
185
163
|
// On the first attempt this resolves the static DNS endpoint for this env —
|
|
186
164
|
// the DNS layer load-balances across the pronode fleet. On a retry
|
|
187
165
|
// (`retryContext.attempt > 1`) the previously used pronode errored, so we
|
|
188
166
|
// pick a random provider straight from the list instead of re-pinning the
|
|
189
167
|
// same one. `pickIpMode(config)` honours the dapp's data-ipv4 / data-ipv6
|
|
190
168
|
// preference so frictionless and the subsequent captcha hops stay on the
|
|
191
|
-
// same stack.
|
|
192
|
-
//
|
|
193
|
-
// This await usually hits the in-flight/resolved promise warmed at the
|
|
194
|
-
// top of this function — see the prefetch call there.
|
|
169
|
+
// same stack. Resolved up front — before detection rather than alongside it —
|
|
170
|
+
// because the detector bundle is served BY this provider.
|
|
195
171
|
const provider = await getProcaptchaRandomActiveProvider(
|
|
196
172
|
config.defaultEnvironment,
|
|
197
173
|
ipMode,
|
|
@@ -203,6 +179,87 @@ const customDetectBot: BotDetectionFunction = async (
|
|
|
203
179
|
config.account.address,
|
|
204
180
|
);
|
|
205
181
|
|
|
182
|
+
// Ask the provider for a per-session detector bundle. The detector lives ONLY
|
|
183
|
+
// in the provider-served pool: when the provider has a populated pool it
|
|
184
|
+
// returns the obfuscated detector (each with its own keys + inner cipher)
|
|
185
|
+
// plus a detectorSessionId. When it cannot (no pool, network, decode, or
|
|
186
|
+
// timeout) we have NO detector to run — there is no bundled fallback — so we
|
|
187
|
+
// signal `detectorUnavailable` and the provider serves a PoW challenge.
|
|
188
|
+
let detectorSessionId: string | undefined;
|
|
189
|
+
let providerDetect: DetectorType | undefined;
|
|
190
|
+
try {
|
|
191
|
+
const assigned = await withTimeout(
|
|
192
|
+
providerApi.assignDetectorBundle(config.account.address),
|
|
193
|
+
ASSIGN_TIMEOUT_MS,
|
|
194
|
+
);
|
|
195
|
+
if (assigned.useProviderBundle && assigned.detectorScript) {
|
|
196
|
+
detectorSessionId = assigned.detectorSessionId;
|
|
197
|
+
providerDetect = await withTimeout(
|
|
198
|
+
DetectorLoaderFromScript(assigned.detectorScript),
|
|
199
|
+
ASSIGN_TIMEOUT_MS,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// No detector available — fall through to the PoW request below.
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const ExtClass = await extClassPromise;
|
|
207
|
+
const ext = new ExtClass();
|
|
208
|
+
|
|
209
|
+
// No provider detector ⇒ no detection is possible. Request a PoW challenge
|
|
210
|
+
// directly: send no token and the detectorUnavailable flag so the provider
|
|
211
|
+
// serves PoW rather than attempting to score an absent payload.
|
|
212
|
+
if (providerDetect === undefined) {
|
|
213
|
+
const userAccount = await ext.getAccount(config);
|
|
214
|
+
const { currentUrl: fallbackUrl, iframeUrl: fallbackIframeUrl } =
|
|
215
|
+
getCurrentPageUrls();
|
|
216
|
+
const powCaptcha = await withTimeout(
|
|
217
|
+
providerApi.getFrictionlessCaptcha(
|
|
218
|
+
undefined,
|
|
219
|
+
undefined,
|
|
220
|
+
config.account.address,
|
|
221
|
+
userAccount.account.address,
|
|
222
|
+
config.mode,
|
|
223
|
+
undefined,
|
|
224
|
+
undefined,
|
|
225
|
+
true,
|
|
226
|
+
fallbackUrl,
|
|
227
|
+
fallbackIframeUrl,
|
|
228
|
+
),
|
|
229
|
+
10000,
|
|
230
|
+
);
|
|
231
|
+
if (powCaptcha.dns_url) {
|
|
232
|
+
try {
|
|
233
|
+
void fetch(powCaptcha.dns_url, {
|
|
234
|
+
method: "GET",
|
|
235
|
+
mode: "no-cors",
|
|
236
|
+
credentials: "omit",
|
|
237
|
+
keepalive: true,
|
|
238
|
+
cache: "no-store",
|
|
239
|
+
}).catch(() => undefined);
|
|
240
|
+
} catch {
|
|
241
|
+
/* swallow */
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
captchaType: powCaptcha.captchaType,
|
|
246
|
+
sessionId: powCaptcha.sessionId,
|
|
247
|
+
provider: provider,
|
|
248
|
+
status: powCaptcha.status,
|
|
249
|
+
userAccount: userAccount,
|
|
250
|
+
error: powCaptcha.error,
|
|
251
|
+
hp: powCaptcha.hp,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const detect: DetectorType = providerDetect;
|
|
256
|
+
|
|
257
|
+
const detectionResult = await detect(container, restartFn, () =>
|
|
258
|
+
ext.getAccount(config),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const userAccount = detectionResult.userAccount;
|
|
262
|
+
|
|
206
263
|
// SIMD readings deliberately omitted from the frictionless hop. The WASM
|
|
207
264
|
// benchmark is a CPU-bound loop that contends with BotScoreWorker if it
|
|
208
265
|
// runs during detection; deferring it until after the POST is in flight
|
|
@@ -217,6 +274,8 @@ const customDetectBot: BotDetectionFunction = async (
|
|
|
217
274
|
userAccount.account.address,
|
|
218
275
|
config.mode,
|
|
219
276
|
undefined,
|
|
277
|
+
detectorSessionId,
|
|
278
|
+
undefined,
|
|
220
279
|
currentUrl,
|
|
221
280
|
iframeUrl,
|
|
222
281
|
);
|
package/src/detectorLoader.ts
CHANGED
|
@@ -12,7 +12,69 @@
|
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
|
-
type
|
|
15
|
+
import type {
|
|
16
|
+
Account,
|
|
17
|
+
BehavioralData,
|
|
18
|
+
ClickEventPoint,
|
|
19
|
+
MouseMovementPoint,
|
|
20
|
+
PackedBehavioralData,
|
|
21
|
+
TouchEventPoint,
|
|
22
|
+
} from "@prosopo/types";
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
// The detector is NOT bundled into the widget — it lives only in the
|
|
25
|
+
// provider-served pool bundles, loaded at runtime via a blob URL below. So the
|
|
26
|
+
// signature is declared locally here (mirroring `@prosopo/detector`'s default
|
|
27
|
+
// export) from shared @prosopo/types primitives, rather than importing the
|
|
28
|
+
// detector package as a build-time type dependency.
|
|
29
|
+
export type DetectorType = (
|
|
30
|
+
container: HTMLElement | undefined,
|
|
31
|
+
restart: () => void,
|
|
32
|
+
accountGenerator: () => Promise<Account>,
|
|
33
|
+
) => Promise<{
|
|
34
|
+
token: string;
|
|
35
|
+
shadowDomCleanup: () => void;
|
|
36
|
+
encryptHeadHash: string;
|
|
37
|
+
mouseTracker?: {
|
|
38
|
+
start: () => void;
|
|
39
|
+
stop: () => void;
|
|
40
|
+
getData: () => MouseMovementPoint[];
|
|
41
|
+
clear: () => void;
|
|
42
|
+
};
|
|
43
|
+
touchTracker?: {
|
|
44
|
+
start: () => void;
|
|
45
|
+
stop: () => void;
|
|
46
|
+
getData: () => TouchEventPoint[];
|
|
47
|
+
clear: () => void;
|
|
48
|
+
};
|
|
49
|
+
clickTracker?: {
|
|
50
|
+
start: () => void;
|
|
51
|
+
stop: () => void;
|
|
52
|
+
getData: () => ClickEventPoint[];
|
|
53
|
+
clear: () => void;
|
|
54
|
+
};
|
|
55
|
+
hasTouchSupport?: string;
|
|
56
|
+
encryptBehavioralData?: (data: string) => Promise<string>;
|
|
57
|
+
packBehavioralData?: (behavioralData: BehavioralData) => PackedBehavioralData;
|
|
58
|
+
getSimdReadings?: (timeoutMs?: number) => Promise<string | undefined>;
|
|
59
|
+
userAccount: Account;
|
|
60
|
+
}>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Loads a detector from a provider-served obfuscated ESM string (the per-session
|
|
64
|
+
* pool bundle). Evaluated via a blob URL — the served module is self-contained
|
|
65
|
+
* (no imports), so dynamic `import()` of the blob yields its default export.
|
|
66
|
+
*/
|
|
67
|
+
export const DetectorLoaderFromScript = async (
|
|
68
|
+
script: string,
|
|
69
|
+
): Promise<DetectorType> => {
|
|
70
|
+
const blob = new Blob([script], { type: "text/javascript" });
|
|
71
|
+
const url = URL.createObjectURL(blob);
|
|
72
|
+
try {
|
|
73
|
+
const mod = (await import(/* @vite-ignore */ url)) as {
|
|
74
|
+
default: DetectorType;
|
|
75
|
+
};
|
|
76
|
+
return mod.default;
|
|
77
|
+
} finally {
|
|
78
|
+
URL.revokeObjectURL(url);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
@@ -19,6 +19,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
19
19
|
const mocks = vi.hoisted(() => ({
|
|
20
20
|
getFrictionlessCaptcha: vi.fn(),
|
|
21
21
|
getProcaptchaRandomActiveProvider: vi.fn(),
|
|
22
|
+
assignDetectorBundle: vi.fn(),
|
|
22
23
|
detect: vi.fn(),
|
|
23
24
|
}));
|
|
24
25
|
|
|
@@ -29,6 +30,7 @@ vi.mock("@prosopo/api", () => ({
|
|
|
29
30
|
ProviderApi: vi.fn(function () {
|
|
30
31
|
return {
|
|
31
32
|
getFrictionlessCaptcha: mocks.getFrictionlessCaptcha,
|
|
33
|
+
assignDetectorBundle: mocks.assignDetectorBundle,
|
|
32
34
|
};
|
|
33
35
|
}),
|
|
34
36
|
}));
|
|
@@ -48,7 +50,7 @@ vi.mock("@prosopo/procaptcha-common", () => ({
|
|
|
48
50
|
}));
|
|
49
51
|
|
|
50
52
|
vi.mock("../detectorLoader.js", () => ({
|
|
51
|
-
|
|
53
|
+
DetectorLoaderFromScript: vi.fn(async () => mocks.detect),
|
|
52
54
|
}));
|
|
53
55
|
|
|
54
56
|
import customDetectBot from "../customDetectBot.js";
|
|
@@ -81,11 +83,19 @@ const captchaResponse = {
|
|
|
81
83
|
beforeEach(() => {
|
|
82
84
|
mocks.getFrictionlessCaptcha.mockReset();
|
|
83
85
|
mocks.getProcaptchaRandomActiveProvider.mockReset();
|
|
86
|
+
mocks.assignDetectorBundle.mockReset();
|
|
84
87
|
mocks.detect.mockReset();
|
|
85
88
|
mocks.getProcaptchaRandomActiveProvider.mockResolvedValue({
|
|
86
89
|
providerAccount: "dns-routed",
|
|
87
90
|
provider: { url: "https://provider.test" },
|
|
88
91
|
});
|
|
92
|
+
// Provider serves a per-session detector bundle (the only detector source).
|
|
93
|
+
mocks.assignDetectorBundle.mockResolvedValue({
|
|
94
|
+
useProviderBundle: true,
|
|
95
|
+
detectorSessionId: "det-1",
|
|
96
|
+
detectorScript: "SELF_CONTAINED_ESM",
|
|
97
|
+
status: "ok",
|
|
98
|
+
});
|
|
89
99
|
mocks.getFrictionlessCaptcha.mockResolvedValue(captchaResponse);
|
|
90
100
|
});
|
|
91
101
|
|
package/tsconfig.cjs.json
CHANGED