@prosopo/procaptcha 2.10.57 → 2.10.58

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.
@@ -1,388 +1,289 @@
1
+ import { ProsopoCaptchaApi } from "./ProsopoCaptchaApi.js";
1
2
  import { stringToHex } from "@polkadot/util/string";
2
3
  import { ProviderApi } from "@prosopo/api";
3
- import { ProsopoDatasetError, ProsopoError, ProsopoEnvError } from "@prosopo/common";
4
- import { getDefaultEvents, buildUpdateState, providerRetry, getProcaptchaRandomActiveProvider, pickIpMode, ExtensionLoader } from "@prosopo/procaptcha-common";
5
- import { ProcaptchaConfigSchema, ApiParams, encodeProcaptchaOutput, CaptchaType } from "@prosopo/types";
6
- import { sleep, at, embedData, hashToHex } from "@prosopo/util";
4
+ import { ProsopoDatasetError, ProsopoEnvError, ProsopoError } from "@prosopo/common";
5
+ import { ExtensionLoader, buildUpdateState, getDefaultEvents, getProcaptchaRandomActiveProvider, getSimdReadingsForSubmit, pickIpMode, providerRetry } from "@prosopo/procaptcha-common";
6
+ import { ApiParams, CaptchaType, ProcaptchaConfigSchema, encodeProcaptchaOutput } from "@prosopo/types";
7
+ import { at, embedData, hashToHex, sleep } from "@prosopo/util";
7
8
  import { randomAsHex } from "@prosopo/util-crypto";
8
- import { ProsopoCaptchaApi } from "./ProsopoCaptchaApi.js";
9
- const defaultState = () => {
10
- return {
11
- // note order matters! see buildUpdateState. These fields are set in order, so disable modal first, then set loading to false, etc.
12
- showModal: false,
13
- loading: false,
14
- index: 0,
15
- challenge: void 0,
16
- solutions: void 0,
17
- isHuman: false,
18
- captchaApi: void 0,
19
- account: void 0
20
- // don't handle timeout here, this should be handled by the state management
21
- };
9
+ //#region src/modules/Manager.ts
10
+ var defaultState = () => {
11
+ return {
12
+ showModal: false,
13
+ loading: false,
14
+ index: 0,
15
+ challenge: void 0,
16
+ solutions: void 0,
17
+ isHuman: false,
18
+ captchaApi: void 0,
19
+ account: void 0
20
+ };
22
21
  };
22
+ /**
23
+ * The state operator. This is used to mutate the state of Procaptcha during the captcha process. State updates are published via the onStateUpdate callback. This should be used by frontends, e.g. react, to maintain the state of Procaptcha across renders.
24
+ */
23
25
  function Manager(configOptional, state, onStateUpdate, callbacks, frictionlessState, getHoneypotValue) {
24
- const events = getDefaultEvents(callbacks);
25
- const updateState = buildUpdateState(state, onStateUpdate);
26
- let checkboxClickX = 0;
27
- let checkboxClickY = 0;
28
- let previousProviderUrl;
29
- const getConfig = () => {
30
- const config = {
31
- userAccountAddress: "",
32
- ...configOptional
33
- };
34
- if (state.account) {
35
- config.userAccountAddress = state.account.account.address;
36
- }
37
- return ProcaptchaConfigSchema.parse(config);
38
- };
39
- const start = async (checkboxX = 0, checkboxY = 0) => {
40
- checkboxClickX = checkboxX;
41
- checkboxClickY = checkboxY;
42
- events.onOpen();
43
- await providerRetry(
44
- async () => {
45
- if (state.loading) {
46
- return;
47
- }
48
- if (state.isHuman) {
49
- return;
50
- }
51
- updateState({ loading: true });
52
- updateState({
53
- attemptCount: state.attemptCount ? state.attemptCount + 1 : 1
54
- });
55
- updateState({
56
- sessionId: frictionlessState?.sessionId
57
- });
58
- const config = getConfig();
59
- updateState({ dappAccount: config.account.address });
60
- await sleep(100);
61
- const account = await loadAccount();
62
- let captchaApi = state.captchaApi;
63
- if (!frictionlessState?.provider) {
64
- const currentConfig = getConfig();
65
- const getRandomProviderResponse = await getProcaptchaRandomActiveProvider(
66
- currentConfig.defaultEnvironment,
67
- pickIpMode(currentConfig),
68
- { attempt: state.attemptCount, excludeUrl: previousProviderUrl }
69
- );
70
- const providerUrl = getRandomProviderResponse.provider.url;
71
- previousProviderUrl = providerUrl;
72
- const providerApi = await loadProviderApi(providerUrl);
73
- captchaApi = new ProsopoCaptchaApi(
74
- account.account.address,
75
- getRandomProviderResponse,
76
- providerApi,
77
- config.web2,
78
- config.account.address || ""
79
- );
80
- updateState({ captchaApi });
81
- } else {
82
- const providerUrl = frictionlessState.provider.provider.url;
83
- const providerApi = await loadProviderApi(providerUrl);
84
- captchaApi = new ProsopoCaptchaApi(
85
- account.account.address,
86
- frictionlessState.provider,
87
- providerApi,
88
- config.web2,
89
- config.account.address || ""
90
- );
91
- updateState({ captchaApi });
92
- }
93
- const simdReadingsOnChallenge = frictionlessState?.getSimdReadings ? await frictionlessState.getSimdReadings(0) : void 0;
94
- const challenge = await captchaApi?.getCaptchaChallenge(
95
- state.sessionId,
96
- simdReadingsOnChallenge
97
- );
98
- if (challenge.error) {
99
- updateState({
100
- loading: false,
101
- error: {
102
- message: challenge.error.message,
103
- key: challenge.error.key || "API.UNKNOWN_ERROR"
104
- }
105
- });
106
- events.onError(new Error(challenge.error?.message));
107
- } else {
108
- if (challenge.captchas.length <= 0) {
109
- throw new ProsopoDatasetError("DEVELOPER.PROVIDER_NO_CAPTCHA");
110
- }
111
- const timeMillis = challenge.captchas.map(
112
- (captcha) => captcha.timeLimitMs || config.captchas.image.challengeTimeout
113
- ).reduce((a, b) => a + b);
114
- const timeout = setTimeout(() => {
115
- events.onChallengeExpired();
116
- updateState({ isHuman: false, showModal: false, loading: false });
117
- }, timeMillis);
118
- updateState({
119
- index: 0,
120
- solutions: challenge.captchas.map(() => []),
121
- challenge,
122
- showModal: true,
123
- timeout,
124
- loading: false
125
- });
126
- }
127
- },
128
- start,
129
- resetState,
130
- state.attemptCount,
131
- 10
132
- );
133
- };
134
- const submit = async () => {
135
- await providerRetry(
136
- async () => {
137
- clearTimeout();
138
- if (!state.challenge) {
139
- throw new ProsopoError("CAPTCHA.NO_CAPTCHA", {
140
- context: { error: "Cannot submit, no Captcha found in state" }
141
- });
142
- }
143
- updateState({ showModal: false });
144
- const challenge = state.challenge;
145
- const captchaSolution = state.challenge.captchas.map(
146
- (captcha, index) => {
147
- const solution = at(state.solutions, index);
148
- const shapeCoords = solution.flatMap(([_, x, y]) => [x, y]);
149
- const coords = index === 0 ? [checkboxClickX, checkboxClickY, ...shapeCoords] : shapeCoords;
150
- const salt = randomAsHex(
151
- coords.map((x) => x.toString(16).length + 4).reduce((acc, curr) => acc + curr, 0)
152
- );
153
- const saltCoord = embedData(salt, coords);
154
- return {
155
- captchaId: captcha.captchaId,
156
- captchaContentId: captcha.captchaContentId,
157
- salt: saltCoord,
158
- solution: solution.flatMap((s) => s[0])
159
- };
160
- }
161
- );
162
- const account = getAccount();
163
- const signer = getExtension(account).signer;
164
- const first = at(challenge.captchas, 0);
165
- if (!first.datasetId) {
166
- throw new ProsopoDatasetError("CAPTCHA.INVALID_CAPTCHA_ID", {
167
- context: { error: "No datasetId set for challenge" }
168
- });
169
- }
170
- const captchaApi = state.captchaApi;
171
- if (!captchaApi) {
172
- throw new ProsopoError("CAPTCHA.INVALID_TOKEN", {
173
- context: { error: "No Captcha API found in state" }
174
- });
175
- }
176
- if (!signer || !signer.signRaw) {
177
- throw new ProsopoEnvError("GENERAL.CANT_FIND_KEYRINGPAIR", {
178
- context: {
179
- error: "Signer is not defined, cannot sign message to prove account ownership"
180
- }
181
- });
182
- }
183
- const userTimestampSignature = await signer.signRaw({
184
- address: account.account.address,
185
- data: stringToHex(challenge[ApiParams.timestamp]),
186
- type: "bytes"
187
- });
188
- let encryptedBehavioralData;
189
- if (frictionlessState?.encryptBehavioralData && (frictionlessState?.behaviorCollector1 || frictionlessState?.behaviorCollector2 || frictionlessState?.behaviorCollector3)) {
190
- try {
191
- const behavioralData = {
192
- collector1: frictionlessState.behaviorCollector1?.getData() || [],
193
- collector2: frictionlessState.behaviorCollector2?.getData() || [],
194
- collector3: frictionlessState.behaviorCollector3?.getData() || [],
195
- deviceCapability: frictionlessState.deviceCapability || "unknown"
196
- };
197
- const dataToEncrypt = frictionlessState.packBehavioralData ? frictionlessState.packBehavioralData(behavioralData) : behavioralData;
198
- encryptedBehavioralData = await frictionlessState.encryptBehavioralData(
199
- JSON.stringify(dataToEncrypt)
200
- );
201
- } catch {
202
- }
203
- }
204
- const simdReadings = frictionlessState?.getSimdReadings ? await frictionlessState.getSimdReadings() : void 0;
205
- const hpValue = getHoneypotValue?.();
206
- const clientMetaData = hpValue ? { hp: hpValue } : void 0;
207
- const submission = await captchaApi.submitCaptchaSolution(
208
- userTimestampSignature.signature,
209
- challenge.requestHash,
210
- captchaSolution,
211
- challenge.timestamp,
212
- challenge.signature.provider.requestHash,
213
- encryptedBehavioralData,
214
- simdReadings,
215
- clientMetaData
216
- );
217
- const isHuman = submission[0].verified;
218
- updateState({
219
- submission,
220
- isHuman,
221
- loading: false
222
- });
223
- if (state.isHuman) {
224
- const providerUrl = captchaApi.provider.provider.url;
225
- events.onHuman(
226
- encodeProcaptchaOutput({
227
- [ApiParams.providerUrl]: providerUrl,
228
- [ApiParams.user]: account.account.address,
229
- [ApiParams.dapp]: getDappAccount(),
230
- [ApiParams.commitmentId]: hashToHex(submission[1]),
231
- [ApiParams.timestamp]: challenge.timestamp,
232
- [ApiParams.signature]: {
233
- [ApiParams.provider]: {
234
- [ApiParams.requestHash]: challenge.signature.provider.requestHash
235
- },
236
- [ApiParams.user]: {
237
- [ApiParams.timestamp]: userTimestampSignature.signature
238
- }
239
- },
240
- [ApiParams.captchaType]: CaptchaType.image
241
- })
242
- );
243
- setValidChallengeTimeout();
244
- } else {
245
- events.onFailed();
246
- resetState(frictionlessState?.restart);
247
- }
248
- },
249
- start,
250
- resetState,
251
- state.attemptCount,
252
- 10
253
- );
254
- };
255
- const cancel = async () => {
256
- clearTimeout();
257
- resetState(frictionlessState?.restart);
258
- events.onClose();
259
- };
260
- const reload = async () => {
261
- clearTimeout();
262
- events.onReload();
263
- resetState(frictionlessState?.restart);
264
- if (!frictionlessState?.restart) {
265
- await start();
266
- }
267
- };
268
- const select = (hash, x, y) => {
269
- if (!state.challenge) {
270
- throw new ProsopoError("CAPTCHA.NO_CAPTCHA", {
271
- context: { error: "Cannot select, no Captcha found in state" }
272
- });
273
- }
274
- if (state.index >= state.challenge.captchas.length || state.index < 0) {
275
- throw new ProsopoError("CAPTCHA.NO_CAPTCHA", {
276
- context: {
277
- error: "Cannot select, index is out of range for this Captcha"
278
- }
279
- });
280
- }
281
- const index = state.index;
282
- const solutions = state.solutions;
283
- const solution = at(solutions, index);
284
- if (solution.some((s) => s[0] === hash)) {
285
- const newSolution = solution.filter((s) => s[0] !== hash);
286
- solutions[index] = newSolution;
287
- } else {
288
- solutions[index] = [...solution, [hash, x || 0, y || 0]];
289
- }
290
- updateState({ solutions });
291
- };
292
- const nextRound = () => {
293
- if (!state.challenge) {
294
- throw new ProsopoError("CAPTCHA.NO_CAPTCHA", {
295
- context: { error: "Cannot select, no Captcha found in state" }
296
- });
297
- }
298
- if (state.index + 1 >= state.challenge.captchas.length) {
299
- throw new ProsopoError("CAPTCHA.NO_CAPTCHA", {
300
- context: {
301
- error: "Cannot select, index is out of range for this Captcha"
302
- }
303
- });
304
- }
305
- updateState({ index: state.index + 1 });
306
- };
307
- const loadProviderApi = async (providerUrl) => {
308
- const config = getConfig();
309
- if (!config.account.address) {
310
- throw new ProsopoEnvError("GENERAL.SITE_KEY_MISSING");
311
- }
312
- return new ProviderApi(providerUrl, config.account.address);
313
- };
314
- const clearTimeout = () => {
315
- window.clearTimeout(Number(state.timeout));
316
- updateState({ timeout: void 0 });
317
- };
318
- const setValidChallengeTimeout = () => {
319
- const timeMillis = configOptional.captchas.image.solutionTimeout;
320
- const successfullChallengeTimeout = setTimeout(() => {
321
- updateState({ isHuman: false });
322
- events.onExpired();
323
- }, timeMillis);
324
- updateState({ successfullChallengeTimeout });
325
- };
326
- const resetState = (frictionlessRestart) => {
327
- clearTimeout();
328
- updateState(defaultState());
329
- events.onReset();
330
- if (frictionlessRestart) {
331
- frictionlessRestart();
332
- }
333
- };
334
- const loadAccount = async () => {
335
- const config = getConfig();
336
- if (!config.web2 && !config.userAccountAddress) {
337
- throw new ProsopoEnvError("GENERAL.ACCOUNT_NOT_FOUND", {
338
- context: { error: "Account address has not been set for web3 mode" }
339
- });
340
- }
341
- const selectAccount = async () => {
342
- const ext = new (await ExtensionLoader(config.web2))();
343
- if (frictionlessState) {
344
- return frictionlessState.userAccount;
345
- }
346
- return await ext.getAccount(config);
347
- };
348
- const account = await selectAccount();
349
- updateState({ account });
350
- return getAccount();
351
- };
352
- const getAccount = () => {
353
- if (!state.account) {
354
- throw new ProsopoEnvError("GENERAL.ACCOUNT_NOT_FOUND", {
355
- context: { error: "Account not loaded" }
356
- });
357
- }
358
- const account = state.account;
359
- return account;
360
- };
361
- const getDappAccount = () => {
362
- if (!state.dappAccount) {
363
- throw new ProsopoEnvError("GENERAL.SITE_KEY_MISSING");
364
- }
365
- const dappAccount = state.dappAccount;
366
- return dappAccount;
367
- };
368
- const getExtension = (possiblyAccount) => {
369
- const account = possiblyAccount || getAccount();
370
- if (!account.extension) {
371
- throw new ProsopoEnvError("ACCOUNT.NO_POLKADOT_EXTENSION", {
372
- context: { error: "Extension not loaded" }
373
- });
374
- }
375
- return account.extension;
376
- };
377
- return {
378
- start,
379
- cancel,
380
- submit,
381
- select,
382
- nextRound,
383
- reload
384
- };
26
+ const events = getDefaultEvents(callbacks);
27
+ const updateState = buildUpdateState(state, onStateUpdate);
28
+ let checkboxClickX = 0;
29
+ let checkboxClickY = 0;
30
+ let previousProviderUrl;
31
+ /**
32
+ * Build the config on demand, using the optional config passed in from the outside. State may override various
33
+ * config values depending on the state of the captcha process. E.g. if the process has been started using account
34
+ * "ABC" and then the user changes account to "DEF" via the optional config prop, the account in use will not change.
35
+ * This is because the captcha process has already been started using account "ABC".
36
+ * @returns the config for procaptcha
37
+ */
38
+ const getConfig = () => {
39
+ const config = {
40
+ userAccountAddress: "",
41
+ ...configOptional
42
+ };
43
+ if (state.account) config.userAccountAddress = state.account.account.address;
44
+ return ProcaptchaConfigSchema.parse(config);
45
+ };
46
+ /**
47
+ * Called on start of user verification. This is when the user ticks the box to claim they are human.
48
+ */
49
+ const start = async (checkboxX = 0, checkboxY = 0) => {
50
+ checkboxClickX = checkboxX;
51
+ checkboxClickY = checkboxY;
52
+ events.onOpen();
53
+ await providerRetry(async () => {
54
+ if (state.loading) return;
55
+ if (state.isHuman) return;
56
+ updateState({ loading: true });
57
+ updateState({ attemptCount: state.attemptCount ? state.attemptCount + 1 : 1 });
58
+ updateState({ sessionId: frictionlessState?.sessionId });
59
+ const config = getConfig();
60
+ updateState({ dappAccount: config.account.address });
61
+ await sleep(100);
62
+ const account = await loadAccount();
63
+ let captchaApi = state.captchaApi;
64
+ if (!frictionlessState?.provider) {
65
+ const currentConfig = getConfig();
66
+ const getRandomProviderResponse = await getProcaptchaRandomActiveProvider(currentConfig.defaultEnvironment, pickIpMode(currentConfig), {
67
+ attempt: state.attemptCount,
68
+ excludeUrl: previousProviderUrl
69
+ });
70
+ const providerUrl = getRandomProviderResponse.provider.url;
71
+ previousProviderUrl = providerUrl;
72
+ const providerApi = await loadProviderApi(providerUrl);
73
+ captchaApi = new ProsopoCaptchaApi(account.account.address, getRandomProviderResponse, providerApi, config.web2, config.account.address || "");
74
+ updateState({ captchaApi });
75
+ } else {
76
+ const providerUrl = frictionlessState.provider.provider.url;
77
+ const providerApi = await loadProviderApi(providerUrl);
78
+ captchaApi = new ProsopoCaptchaApi(account.account.address, frictionlessState.provider, providerApi, config.web2, config.account.address || "");
79
+ updateState({ captchaApi });
80
+ }
81
+ const simdReadingsOnChallenge = frictionlessState?.getSimdReadings ? await frictionlessState.getSimdReadings(0) : void 0;
82
+ const challenge = await captchaApi?.getCaptchaChallenge(state.sessionId, simdReadingsOnChallenge);
83
+ if (challenge.error) {
84
+ updateState({
85
+ loading: false,
86
+ error: {
87
+ message: challenge.error.message,
88
+ key: challenge.error.key || "API.UNKNOWN_ERROR"
89
+ }
90
+ });
91
+ events.onError(new Error(challenge.error?.message));
92
+ } else {
93
+ if (challenge.captchas.length <= 0) throw new ProsopoDatasetError("DEVELOPER.PROVIDER_NO_CAPTCHA");
94
+ const timeMillis = challenge.captchas.map((captcha) => captcha.timeLimitMs || config.captchas.image.challengeTimeout).reduce((a, b) => a + b);
95
+ const timeout = setTimeout(() => {
96
+ events.onChallengeExpired();
97
+ updateState({
98
+ isHuman: false,
99
+ showModal: false,
100
+ loading: false
101
+ });
102
+ }, timeMillis);
103
+ updateState({
104
+ index: 0,
105
+ solutions: challenge.captchas.map(() => []),
106
+ challenge,
107
+ showModal: true,
108
+ timeout,
109
+ loading: false
110
+ });
111
+ }
112
+ }, start, resetState, state.attemptCount, 10);
113
+ };
114
+ const submit = async () => {
115
+ await providerRetry(async () => {
116
+ clearTimeout();
117
+ if (!state.challenge) throw new ProsopoError("CAPTCHA.NO_CAPTCHA", { context: { error: "Cannot submit, no Captcha found in state" } });
118
+ updateState({ showModal: false });
119
+ const challenge = state.challenge;
120
+ const captchaSolution = state.challenge.captchas.map((captcha, index) => {
121
+ const solution = at(state.solutions, index);
122
+ const shapeCoords = solution.flatMap(([_, x, y]) => [x, y]);
123
+ const coords = index === 0 ? [
124
+ checkboxClickX,
125
+ checkboxClickY,
126
+ ...shapeCoords
127
+ ] : shapeCoords;
128
+ const saltCoord = embedData(randomAsHex(coords.map((x) => x.toString(16).length + 4).reduce((acc, curr) => acc + curr, 0)), coords);
129
+ return {
130
+ captchaId: captcha.captchaId,
131
+ captchaContentId: captcha.captchaContentId,
132
+ salt: saltCoord,
133
+ solution: solution.flatMap((s) => s[0])
134
+ };
135
+ });
136
+ const account = getAccount();
137
+ const signer = getExtension(account).signer;
138
+ if (!at(challenge.captchas, 0).datasetId) throw new ProsopoDatasetError("CAPTCHA.INVALID_CAPTCHA_ID", { context: { error: "No datasetId set for challenge" } });
139
+ const captchaApi = state.captchaApi;
140
+ if (!captchaApi) throw new ProsopoError("CAPTCHA.INVALID_TOKEN", { context: { error: "No Captcha API found in state" } });
141
+ if (!signer || !signer.signRaw) throw new ProsopoEnvError("GENERAL.CANT_FIND_KEYRINGPAIR", { context: { error: "Signer is not defined, cannot sign message to prove account ownership" } });
142
+ const userTimestampSignature = await signer.signRaw({
143
+ address: account.account.address,
144
+ data: stringToHex(challenge[ApiParams.timestamp]),
145
+ type: "bytes"
146
+ });
147
+ let encryptedBehavioralData;
148
+ if (frictionlessState?.encryptBehavioralData && (frictionlessState?.behaviorCollector1 || frictionlessState?.behaviorCollector2 || frictionlessState?.behaviorCollector3)) try {
149
+ const behavioralData = {
150
+ collector1: frictionlessState.behaviorCollector1?.getData() || [],
151
+ collector2: frictionlessState.behaviorCollector2?.getData() || [],
152
+ collector3: frictionlessState.behaviorCollector3?.getData() || [],
153
+ deviceCapability: frictionlessState.deviceCapability || "unknown"
154
+ };
155
+ const dataToEncrypt = frictionlessState.packBehavioralData ? frictionlessState.packBehavioralData(behavioralData) : behavioralData;
156
+ encryptedBehavioralData = await frictionlessState.encryptBehavioralData(JSON.stringify(dataToEncrypt));
157
+ } catch {}
158
+ const simdReadings = await getSimdReadingsForSubmit(frictionlessState);
159
+ const hpValue = getHoneypotValue?.();
160
+ const clientMetaData = hpValue ? { hp: hpValue } : void 0;
161
+ const submission = await captchaApi.submitCaptchaSolution(userTimestampSignature.signature, challenge.requestHash, captchaSolution, challenge.timestamp, challenge.signature.provider.requestHash, encryptedBehavioralData, simdReadings, clientMetaData);
162
+ const isHuman = submission[0].verified;
163
+ updateState({
164
+ submission,
165
+ isHuman,
166
+ loading: false
167
+ });
168
+ if (state.isHuman) {
169
+ const providerUrl = captchaApi.provider.provider.url;
170
+ events.onHuman(encodeProcaptchaOutput({
171
+ [ApiParams.providerUrl]: providerUrl,
172
+ [ApiParams.user]: account.account.address,
173
+ [ApiParams.dapp]: getDappAccount(),
174
+ [ApiParams.commitmentId]: hashToHex(submission[1]),
175
+ [ApiParams.timestamp]: challenge.timestamp,
176
+ [ApiParams.signature]: {
177
+ [ApiParams.provider]: { [ApiParams.requestHash]: challenge.signature.provider.requestHash },
178
+ [ApiParams.user]: { [ApiParams.timestamp]: userTimestampSignature.signature }
179
+ },
180
+ [ApiParams.captchaType]: CaptchaType.image
181
+ }));
182
+ setValidChallengeTimeout();
183
+ } else {
184
+ events.onFailed();
185
+ resetState(frictionlessState?.restart);
186
+ }
187
+ }, start, resetState, state.attemptCount, 10);
188
+ };
189
+ const cancel = async () => {
190
+ clearTimeout();
191
+ resetState(frictionlessState?.restart);
192
+ events.onClose();
193
+ };
194
+ const reload = async () => {
195
+ clearTimeout();
196
+ events.onReload();
197
+ resetState(frictionlessState?.restart);
198
+ if (!frictionlessState?.restart) await start();
199
+ };
200
+ /**
201
+ * (De)Select an image from the solution for the current round. If the hash is already in the solutions list, it will be removed (deselected) and if not it will be added (selected).
202
+ * @param hash the hash of the image
203
+ * @param x
204
+ * @param y
205
+ */
206
+ const select = (hash, x, y) => {
207
+ if (!state.challenge) throw new ProsopoError("CAPTCHA.NO_CAPTCHA", { context: { error: "Cannot select, no Captcha found in state" } });
208
+ if (state.index >= state.challenge.captchas.length || state.index < 0) throw new ProsopoError("CAPTCHA.NO_CAPTCHA", { context: { error: "Cannot select, index is out of range for this Captcha" } });
209
+ const index = state.index;
210
+ const solutions = state.solutions;
211
+ const solution = at(solutions, index);
212
+ if (solution.some((s) => s[0] === hash)) solutions[index] = solution.filter((s) => s[0] !== hash);
213
+ else solutions[index] = [...solution, [
214
+ hash,
215
+ x || 0,
216
+ y || 0
217
+ ]];
218
+ updateState({ solutions });
219
+ };
220
+ /**
221
+ * Proceed to the next round of the challenge.
222
+ */
223
+ const nextRound = () => {
224
+ if (!state.challenge) throw new ProsopoError("CAPTCHA.NO_CAPTCHA", { context: { error: "Cannot select, no Captcha found in state" } });
225
+ if (state.index + 1 >= state.challenge.captchas.length) throw new ProsopoError("CAPTCHA.NO_CAPTCHA", { context: { error: "Cannot select, index is out of range for this Captcha" } });
226
+ updateState({ index: state.index + 1 });
227
+ };
228
+ const loadProviderApi = async (providerUrl) => {
229
+ const config = getConfig();
230
+ if (!config.account.address) throw new ProsopoEnvError("GENERAL.SITE_KEY_MISSING");
231
+ return new ProviderApi(providerUrl, config.account.address);
232
+ };
233
+ const clearTimeout = () => {
234
+ window.clearTimeout(Number(state.timeout));
235
+ updateState({ timeout: void 0 });
236
+ };
237
+ const setValidChallengeTimeout = () => {
238
+ const timeMillis = configOptional.captchas.image.solutionTimeout;
239
+ const successfullChallengeTimeout = setTimeout(() => {
240
+ updateState({ isHuman: false });
241
+ events.onExpired();
242
+ }, timeMillis);
243
+ updateState({ successfullChallengeTimeout });
244
+ };
245
+ const resetState = (frictionlessRestart) => {
246
+ clearTimeout();
247
+ updateState(defaultState());
248
+ events.onReset();
249
+ if (frictionlessRestart) frictionlessRestart();
250
+ };
251
+ /**
252
+ * Load the account using address specified in config, or generate new address if not found in local storage for web2 mode.
253
+ */
254
+ const loadAccount = async () => {
255
+ const config = getConfig();
256
+ if (!config.web2 && !config.userAccountAddress) throw new ProsopoEnvError("GENERAL.ACCOUNT_NOT_FOUND", { context: { error: "Account address has not been set for web3 mode" } });
257
+ const selectAccount = async () => {
258
+ const ext = new (await (ExtensionLoader(config.web2)))();
259
+ if (frictionlessState) return frictionlessState.userAccount;
260
+ return await ext.getAccount(config);
261
+ };
262
+ const account = await selectAccount();
263
+ updateState({ account });
264
+ return getAccount();
265
+ };
266
+ const getAccount = () => {
267
+ if (!state.account) throw new ProsopoEnvError("GENERAL.ACCOUNT_NOT_FOUND", { context: { error: "Account not loaded" } });
268
+ return state.account;
269
+ };
270
+ const getDappAccount = () => {
271
+ if (!state.dappAccount) throw new ProsopoEnvError("GENERAL.SITE_KEY_MISSING");
272
+ return state.dappAccount;
273
+ };
274
+ const getExtension = (possiblyAccount) => {
275
+ const account = possiblyAccount || getAccount();
276
+ if (!account.extension) throw new ProsopoEnvError("ACCOUNT.NO_POLKADOT_EXTENSION", { context: { error: "Extension not loaded" } });
277
+ return account.extension;
278
+ };
279
+ return {
280
+ start,
281
+ cancel,
282
+ submit,
283
+ select,
284
+ nextRound,
285
+ reload
286
+ };
385
287
  }
386
- export {
387
- Manager
388
- };
288
+ //#endregion
289
+ export { Manager };