@swetrix/captcha 1.0.3 → 2.0.1
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/.prettierrc.js +13 -0
- package/dist/captcha-loader.js +2 -1
- package/dist/captcha-loader.js.map +1 -0
- package/dist/captcha.js +2 -1
- package/dist/captcha.js.map +1 -0
- package/dist/pages/dark.html +138 -102
- package/dist/pages/light.html +138 -95
- package/dist/pages/test.html +2 -2
- package/dist/pow-worker.js +2 -0
- package/dist/pow-worker.js.map +1 -0
- package/package.json +30 -24
- package/rollup.config.mjs +83 -0
- package/src/captcha-loader.ts +13 -43
- package/src/captcha.ts +215 -141
- package/src/pages/dark.html +138 -102
- package/src/pages/light.html +138 -95
- package/src/pages/test.html +2 -2
- package/src/pow-worker.ts +178 -0
- package/tsconfig.esnext.json +6 -8
- package/tsconfig.json +5 -8
- package/dist/esnext/captcha-loader.d.ts +0 -33
- package/dist/esnext/captcha-loader.js +0 -182
- package/dist/esnext/captcha-loader.js.map +0 -1
- package/dist/esnext/captcha.d.ts +0 -37
- package/dist/esnext/captcha.js +0 -248
- package/dist/esnext/captcha.js.map +0 -1
- package/rollup.config.js +0 -52
package/src/captcha.ts
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
+
export {}
|
|
2
|
+
|
|
1
3
|
// @ts-ignore
|
|
2
4
|
const isDevelopment = window.__SWETRIX_CAPTCHA_DEV || false
|
|
3
5
|
|
|
4
6
|
const API_URL = isDevelopment ? 'http://localhost:5005/v1/captcha' : 'https://api.swetrix.com/v1/captcha'
|
|
7
|
+
const WORKER_URL = isDevelopment ? './pow-worker.js' : 'https://cap.swetrix.com/pow-worker.js'
|
|
5
8
|
const MSG_IDENTIFIER = 'swetrix-captcha'
|
|
6
|
-
const DEFAULT_THEME = 'light'
|
|
7
9
|
const CAPTCHA_TOKEN_LIFETIME = 300 // seconds (5 minutes).
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
// Main-thread fallback limits (same as worker)
|
|
12
|
+
const MAX_ITERATIONS = 100_000_000 // 100 million attempts
|
|
13
|
+
const TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
|
|
10
14
|
|
|
11
15
|
const ENDPOINTS = {
|
|
12
|
-
VERIFY: '/verify',
|
|
13
16
|
GENERATE: '/generate',
|
|
14
|
-
|
|
17
|
+
VERIFY: '/verify',
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
enum IFRAME_MESSAGE_TYPES {
|
|
18
21
|
SUCCESS = 'success',
|
|
19
22
|
FAILURE = 'failure',
|
|
20
23
|
TOKEN_EXPIRED = 'tokenExpired',
|
|
21
|
-
MANUAL_STARTED = 'manualStarted',
|
|
22
|
-
MANUAL_FINISHED = 'manualFinished',
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
enum ACTION {
|
|
@@ -29,20 +30,41 @@ enum ACTION {
|
|
|
29
30
|
loading = 'loading',
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
interface PowChallenge {
|
|
34
|
+
challenge: string
|
|
35
|
+
difficulty: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface PowResult {
|
|
39
|
+
type: 'result'
|
|
40
|
+
nonce: number
|
|
41
|
+
solution: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface PowProgress {
|
|
45
|
+
type: 'progress'
|
|
46
|
+
attempts: number
|
|
47
|
+
hashRate: number
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
let activeAction: ACTION = ACTION.checkbox
|
|
51
|
+
let powWorker: Worker | null = null
|
|
33
52
|
|
|
34
53
|
const sendMessageToLoader = (event: IFRAME_MESSAGE_TYPES, data = {}) => {
|
|
35
|
-
window.parent.postMessage(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
window.parent.postMessage(
|
|
55
|
+
{
|
|
56
|
+
event,
|
|
57
|
+
type: MSG_IDENTIFIER,
|
|
58
|
+
// @ts-ignore
|
|
59
|
+
cid: window.__SWETRIX_CAPTCHA_ID,
|
|
60
|
+
...data,
|
|
61
|
+
},
|
|
62
|
+
'*',
|
|
63
|
+
)
|
|
42
64
|
}
|
|
43
65
|
|
|
44
66
|
/**
|
|
45
|
-
* Sets the provided action visible and the rest hidden
|
|
67
|
+
* Sets the provided action visible and the rest hidden with smooth transitions
|
|
46
68
|
* @param {*} action checkbox | failure | completed | loading
|
|
47
69
|
*/
|
|
48
70
|
const activateAction = (action: ACTION) => {
|
|
@@ -50,6 +72,7 @@ const activateAction = (action: ACTION) => {
|
|
|
50
72
|
|
|
51
73
|
const statusDefault = document.querySelector('#status-default')
|
|
52
74
|
const statusFailure = document.querySelector('#status-failure')
|
|
75
|
+
const statusComputing = document.querySelector('#status-computing')
|
|
53
76
|
|
|
54
77
|
const actions = {
|
|
55
78
|
checkbox: document.querySelector('#checkbox'),
|
|
@@ -58,23 +81,39 @@ const activateAction = (action: ACTION) => {
|
|
|
58
81
|
loading: document.querySelector('#loading'),
|
|
59
82
|
}
|
|
60
83
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
84
|
+
// Hide all action elements with transitions
|
|
85
|
+
// Checkbox uses fade-out class for smooth transition
|
|
86
|
+
if (action !== ACTION.checkbox) {
|
|
87
|
+
actions.checkbox?.classList.add('fade-out')
|
|
88
|
+
} else {
|
|
89
|
+
actions.checkbox?.classList.remove('fade-out')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Loading, failure, completed use .show class for visibility
|
|
93
|
+
actions.loading?.classList.remove('show')
|
|
94
|
+
actions.failure?.classList.remove('show')
|
|
95
|
+
actions.completed?.classList.remove('show')
|
|
66
96
|
|
|
67
97
|
// Change the status text
|
|
98
|
+
statusDefault?.classList.add('hidden')
|
|
99
|
+
statusFailure?.classList.add('hidden')
|
|
100
|
+
statusComputing?.classList.add('hidden')
|
|
101
|
+
|
|
68
102
|
if (action === 'failure') {
|
|
69
|
-
statusDefault?.classList.add('hidden')
|
|
70
103
|
statusFailure?.classList.remove('hidden')
|
|
104
|
+
} else if (action === 'loading') {
|
|
105
|
+
statusComputing?.classList.remove('hidden')
|
|
71
106
|
} else {
|
|
72
107
|
statusDefault?.classList.remove('hidden')
|
|
73
|
-
statusFailure?.classList.add('hidden')
|
|
74
108
|
}
|
|
75
109
|
|
|
76
|
-
//
|
|
77
|
-
|
|
110
|
+
// Show the active action element with animation
|
|
111
|
+
if (action !== ACTION.checkbox) {
|
|
112
|
+
// Small delay to ensure CSS transitions work properly
|
|
113
|
+
requestAnimationFrame(() => {
|
|
114
|
+
actions[action]?.classList.add('show')
|
|
115
|
+
})
|
|
116
|
+
}
|
|
78
117
|
}
|
|
79
118
|
|
|
80
119
|
const setLifetimeTimeout = () => {
|
|
@@ -84,41 +123,7 @@ const setLifetimeTimeout = () => {
|
|
|
84
123
|
}, CAPTCHA_TOKEN_LIFETIME * 1000)
|
|
85
124
|
}
|
|
86
125
|
|
|
87
|
-
const
|
|
88
|
-
const manualChallenge = document.querySelector('#manual-challenge')
|
|
89
|
-
const svgCaptcha = document.querySelector('#svg-captcha')
|
|
90
|
-
|
|
91
|
-
if (!svgCaptcha) {
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (!svg) {
|
|
96
|
-
const error = document.createElement('p')
|
|
97
|
-
error.innerText = 'Error loading captcha'
|
|
98
|
-
error.style.color = '#d6292a'
|
|
99
|
-
svgCaptcha?.appendChild(error)
|
|
100
|
-
} else {
|
|
101
|
-
svgCaptcha.innerHTML = svg
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
sendMessageToLoader(IFRAME_MESSAGE_TYPES.MANUAL_STARTED)
|
|
105
|
-
manualChallenge?.classList.remove('hidden')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const disableManualChallenge = () => {
|
|
109
|
-
const manualChallenge = document.querySelector('#manual-challenge')
|
|
110
|
-
const svgCaptcha = document.querySelector('#svg-captcha')
|
|
111
|
-
|
|
112
|
-
if (!svgCaptcha) {
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
sendMessageToLoader(IFRAME_MESSAGE_TYPES.MANUAL_FINISHED)
|
|
117
|
-
svgCaptcha.innerHTML = ''
|
|
118
|
-
manualChallenge?.classList.add('hidden')
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const generateCaptcha = async () => {
|
|
126
|
+
const generateChallenge = async (): Promise<PowChallenge | null> => {
|
|
122
127
|
try {
|
|
123
128
|
const response = await fetch(`${API_URL}${ENDPOINTS.GENERATE}`, {
|
|
124
129
|
method: 'POST',
|
|
@@ -126,27 +131,24 @@ const generateCaptcha = async () => {
|
|
|
126
131
|
'Content-Type': 'application/json',
|
|
127
132
|
},
|
|
128
133
|
body: JSON.stringify({
|
|
129
|
-
// @ts-ignore
|
|
130
|
-
theme: window.__SWETRIX_CAPTCHA_THEME || DEFAULT_THEME,
|
|
131
134
|
// @ts-ignore
|
|
132
135
|
pid: window.__SWETRIX_PROJECT_ID,
|
|
133
136
|
}),
|
|
134
137
|
})
|
|
135
|
-
|
|
138
|
+
|
|
136
139
|
if (!response.ok) {
|
|
137
|
-
throw ''
|
|
140
|
+
throw new Error('Failed to generate challenge')
|
|
138
141
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return data
|
|
142
|
+
|
|
143
|
+
return await response.json()
|
|
142
144
|
} catch (e) {
|
|
143
145
|
sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
|
|
144
146
|
activateAction(ACTION.failure)
|
|
145
|
-
return
|
|
147
|
+
return null
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
const
|
|
151
|
+
const verifySolution = async (challenge: string, nonce: number, solution: string): Promise<string | null> => {
|
|
150
152
|
try {
|
|
151
153
|
const response = await fetch(`${API_URL}${ENDPOINTS.VERIFY}`, {
|
|
152
154
|
method: 'POST',
|
|
@@ -154,99 +156,183 @@ const verify = async () => {
|
|
|
154
156
|
'Content-Type': 'application/json',
|
|
155
157
|
},
|
|
156
158
|
body: JSON.stringify({
|
|
159
|
+
challenge,
|
|
160
|
+
nonce,
|
|
161
|
+
solution,
|
|
157
162
|
// @ts-ignore
|
|
158
163
|
pid: window.__SWETRIX_PROJECT_ID,
|
|
159
164
|
}),
|
|
160
165
|
})
|
|
161
166
|
|
|
162
167
|
if (!response.ok) {
|
|
163
|
-
|
|
168
|
+
throw new Error('Verification failed')
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
const data = await response.json()
|
|
167
|
-
|
|
172
|
+
|
|
173
|
+
if (!data.success) {
|
|
174
|
+
throw new Error('Verification failed')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return data.token
|
|
168
178
|
} catch (e) {
|
|
169
179
|
sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
|
|
170
180
|
activateAction(ACTION.failure)
|
|
171
|
-
return
|
|
181
|
+
return null
|
|
172
182
|
}
|
|
173
183
|
}
|
|
174
184
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
branding?.addEventListener('click', (e: Event) => {
|
|
182
|
-
e.stopPropagation()
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
manualSubmitBtn?.addEventListener('click', async (e: Event) => {
|
|
186
|
-
e.stopPropagation()
|
|
185
|
+
const solveChallenge = async (challenge: PowChallenge): Promise<void> => {
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
// Terminate any existing worker
|
|
188
|
+
if (powWorker) {
|
|
189
|
+
powWorker.terminate()
|
|
190
|
+
}
|
|
187
191
|
|
|
188
|
-
|
|
192
|
+
try {
|
|
193
|
+
powWorker = new Worker(WORKER_URL)
|
|
194
|
+
} catch (e) {
|
|
195
|
+
// Fallback: solve in main thread if worker fails
|
|
196
|
+
solveInMainThread(challenge).then(resolve).catch(reject)
|
|
189
197
|
return
|
|
190
198
|
}
|
|
191
199
|
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
powWorker.onmessage = async (
|
|
201
|
+
event: MessageEvent<
|
|
202
|
+
PowResult | PowProgress | { type: 'timeout'; reason: string } | { type: 'error'; message?: string }
|
|
203
|
+
>,
|
|
204
|
+
) => {
|
|
205
|
+
const data = event.data
|
|
194
206
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
207
|
+
if (data.type === 'progress') {
|
|
208
|
+
return
|
|
209
|
+
}
|
|
198
210
|
|
|
199
|
-
|
|
211
|
+
if (data.type === 'timeout') {
|
|
212
|
+
// Worker timed out or hit max iterations
|
|
213
|
+
console.error('PoW worker timeout:', (data as { type: 'timeout'; reason: string }).reason)
|
|
214
|
+
sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
|
|
215
|
+
activateAction(ACTION.failure)
|
|
216
|
+
powWorker?.terminate()
|
|
217
|
+
powWorker = null
|
|
218
|
+
resolve()
|
|
219
|
+
return
|
|
220
|
+
}
|
|
200
221
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
if (data.type === 'result') {
|
|
223
|
+
// Worker found the solution
|
|
224
|
+
const token = await verifySolution(challenge.challenge, (data as PowResult).nonce, (data as PowResult).solution)
|
|
225
|
+
|
|
226
|
+
if (token) {
|
|
227
|
+
sendMessageToLoader(IFRAME_MESSAGE_TYPES.SUCCESS, { token })
|
|
228
|
+
setLifetimeTimeout()
|
|
229
|
+
activateAction(ACTION.completed)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
powWorker?.terminate()
|
|
233
|
+
powWorker = null
|
|
234
|
+
resolve()
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Handle error message from worker
|
|
239
|
+
if (data.type === 'error') {
|
|
240
|
+
const errorData = data as { type: 'error'; message?: string }
|
|
241
|
+
console.error('PoW worker error message:', errorData.message || 'Unknown error')
|
|
242
|
+
sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
|
|
243
|
+
activateAction(ACTION.failure)
|
|
244
|
+
powWorker?.terminate()
|
|
245
|
+
powWorker = null
|
|
246
|
+
resolve()
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Fallback for unexpected message types
|
|
251
|
+
console.warn('PoW worker received unexpected message type:', (data as { type?: unknown }).type, 'Raw data:', data)
|
|
216
252
|
sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
|
|
217
253
|
activateAction(ACTION.failure)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
254
|
+
powWorker?.terminate()
|
|
255
|
+
powWorker = null
|
|
256
|
+
resolve()
|
|
221
257
|
}
|
|
222
258
|
|
|
223
|
-
|
|
224
|
-
|
|
259
|
+
powWorker.onerror = (error) => {
|
|
260
|
+
console.error('PoW worker error:', error)
|
|
261
|
+
powWorker?.terminate()
|
|
262
|
+
powWorker = null
|
|
263
|
+
|
|
264
|
+
// Fallback to main thread
|
|
265
|
+
solveInMainThread(challenge).then(resolve).catch(reject)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Start the worker
|
|
269
|
+
powWorker.postMessage({
|
|
270
|
+
challenge: challenge.challenge,
|
|
271
|
+
difficulty: challenge.difficulty,
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Fallback solution for environments where workers don't work
|
|
277
|
+
const solveInMainThread = async (challenge: PowChallenge): Promise<void> => {
|
|
278
|
+
const { challenge: challengeStr, difficulty } = challenge
|
|
279
|
+
let nonce = 0
|
|
280
|
+
const startTime = Date.now()
|
|
281
|
+
|
|
282
|
+
const sha256 = async (message: string): Promise<string> => {
|
|
283
|
+
const encoder = new TextEncoder()
|
|
284
|
+
const data = encoder.encode(message)
|
|
285
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
|
|
286
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
|
287
|
+
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const hasValidPrefix = (hash: string, diff: number): boolean => {
|
|
291
|
+
for (let i = 0; i < diff; i++) {
|
|
292
|
+
if (hash[i] !== '0') return false
|
|
293
|
+
}
|
|
294
|
+
return true
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
while (nonce < MAX_ITERATIONS) {
|
|
298
|
+
// Check overall timeout
|
|
299
|
+
const elapsedMs = Date.now() - startTime
|
|
300
|
+
if (elapsedMs >= TIMEOUT_MS) {
|
|
301
|
+
console.error(`PoW main-thread timeout: ${TIMEOUT_MS}ms elapsed after ${nonce} attempts`)
|
|
225
302
|
sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
|
|
226
303
|
activateAction(ACTION.failure)
|
|
227
|
-
// @ts-ignore
|
|
228
|
-
svgCaptchaInput.value = ''
|
|
229
304
|
return
|
|
230
305
|
}
|
|
231
306
|
|
|
232
|
-
const
|
|
307
|
+
const input = `${challengeStr}:${nonce}`
|
|
308
|
+
const hash = await sha256(input)
|
|
233
309
|
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
310
|
+
if (hasValidPrefix(hash, difficulty)) {
|
|
311
|
+
const token = await verifySolution(challengeStr, nonce, hash)
|
|
312
|
+
|
|
313
|
+
if (token) {
|
|
314
|
+
sendMessageToLoader(IFRAME_MESSAGE_TYPES.SUCCESS, { token })
|
|
315
|
+
setLifetimeTimeout()
|
|
316
|
+
activateAction(ACTION.completed)
|
|
317
|
+
}
|
|
240
318
|
return
|
|
241
319
|
}
|
|
242
320
|
|
|
243
|
-
|
|
244
|
-
|
|
321
|
+
nonce++
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Max iterations reached without finding solution
|
|
325
|
+
console.error(`PoW main-thread max iterations reached: ${MAX_ITERATIONS} attempts`)
|
|
326
|
+
sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
|
|
327
|
+
activateAction(ACTION.failure)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
331
|
+
const captchaComponent = document.querySelector('#swetrix-captcha')
|
|
332
|
+
const branding = document.querySelector('#branding')
|
|
245
333
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
activateAction(ACTION.completed)
|
|
249
|
-
disableManualChallenge()
|
|
334
|
+
branding?.addEventListener('click', (e: Event) => {
|
|
335
|
+
e.stopPropagation()
|
|
250
336
|
})
|
|
251
337
|
|
|
252
338
|
captchaComponent?.addEventListener('click', async () => {
|
|
@@ -261,24 +347,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
261
347
|
|
|
262
348
|
activateAction(ACTION.loading)
|
|
263
349
|
|
|
264
|
-
|
|
265
|
-
const { token } = await verify()
|
|
266
|
-
|
|
267
|
-
if (!token) {
|
|
268
|
-
throw ''
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
TOKEN = token
|
|
272
|
-
sendMessageToLoader(IFRAME_MESSAGE_TYPES.SUCCESS, { token })
|
|
273
|
-
setLifetimeTimeout()
|
|
274
|
-
activateAction(ACTION.completed)
|
|
275
|
-
return
|
|
276
|
-
} catch (e) {
|
|
277
|
-
const { data, hash } = await generateCaptcha()
|
|
350
|
+
const challenge = await generateChallenge()
|
|
278
351
|
|
|
279
|
-
|
|
280
|
-
enableManualChallenge(data)
|
|
352
|
+
if (!challenge) {
|
|
281
353
|
return
|
|
282
354
|
}
|
|
355
|
+
|
|
356
|
+
await solveChallenge(challenge)
|
|
283
357
|
})
|
|
284
358
|
})
|