@swetrix/captcha 2.0.2 → 2.3.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.
Files changed (46) hide show
  1. package/README.md +72 -5
  2. package/dist/captcha-loader.cjs +333 -0
  3. package/dist/captcha-loader.cjs.map +1 -0
  4. package/dist/captcha-loader.esm.js +327 -0
  5. package/dist/captcha-loader.esm.js.map +1 -0
  6. package/dist/captcha-loader.js +1 -1
  7. package/dist/captcha-loader.js.map +1 -1
  8. package/dist/captcha.js +1 -1
  9. package/dist/captcha.js.map +1 -1
  10. package/dist/esnext/captcha-loader.d.ts +10 -0
  11. package/dist/esnext/captcha-loader.js +315 -0
  12. package/dist/esnext/captcha-loader.js.map +1 -0
  13. package/dist/esnext/captcha.d.ts +10 -0
  14. package/dist/esnext/captcha.js +491 -0
  15. package/dist/esnext/captcha.js.map +1 -0
  16. package/dist/esnext/i18n.d.ts +22 -0
  17. package/dist/esnext/i18n.js +110 -0
  18. package/dist/esnext/i18n.js.map +1 -0
  19. package/dist/esnext/index.d.ts +7 -0
  20. package/dist/esnext/index.js +7 -0
  21. package/dist/esnext/index.js.map +1 -0
  22. package/dist/esnext/logger.d.ts +6 -0
  23. package/dist/esnext/logger.js +8 -0
  24. package/dist/esnext/logger.js.map +1 -0
  25. package/dist/esnext/pow-worker.d.ts +1 -0
  26. package/dist/esnext/pow-worker.js +127 -0
  27. package/dist/esnext/pow-worker.js.map +1 -0
  28. package/dist/pages/dark.html +461 -276
  29. package/dist/pages/light.html +463 -278
  30. package/dist/pages/test.html +26 -27
  31. package/package.json +58 -32
  32. package/.nvmrc +0 -1
  33. package/.prettierrc.js +0 -13
  34. package/dist/assets/logo_blue.png +0 -0
  35. package/dist/assets/logo_white.png +0 -0
  36. package/rollup.config.mjs +0 -83
  37. package/src/assets/logo_blue.png +0 -0
  38. package/src/assets/logo_white.png +0 -0
  39. package/src/captcha-loader.ts +0 -212
  40. package/src/captcha.ts +0 -358
  41. package/src/pages/dark.html +0 -284
  42. package/src/pages/light.html +0 -286
  43. package/src/pages/test.html +0 -33
  44. package/src/pow-worker.ts +0 -178
  45. package/tsconfig.esnext.json +0 -15
  46. package/tsconfig.json +0 -14
package/src/captcha.ts DELETED
@@ -1,358 +0,0 @@
1
- export {}
2
-
3
- // @ts-ignore
4
- const isDevelopment = window.__SWETRIX_CAPTCHA_DEV || false
5
-
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'
8
- const MSG_IDENTIFIER = 'swetrix-captcha'
9
- const CAPTCHA_TOKEN_LIFETIME = 300 // seconds (5 minutes).
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
14
-
15
- const ENDPOINTS = {
16
- GENERATE: '/generate',
17
- VERIFY: '/verify',
18
- }
19
-
20
- enum IFRAME_MESSAGE_TYPES {
21
- SUCCESS = 'success',
22
- FAILURE = 'failure',
23
- TOKEN_EXPIRED = 'tokenExpired',
24
- }
25
-
26
- enum ACTION {
27
- checkbox = 'checkbox',
28
- failure = 'failure',
29
- completed = 'completed',
30
- loading = 'loading',
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
-
50
- let activeAction: ACTION = ACTION.checkbox
51
- let powWorker: Worker | null = null
52
-
53
- const sendMessageToLoader = (event: IFRAME_MESSAGE_TYPES, data = {}) => {
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
- )
64
- }
65
-
66
- /**
67
- * Sets the provided action visible and the rest hidden with smooth transitions
68
- * @param {*} action checkbox | failure | completed | loading
69
- */
70
- const activateAction = (action: ACTION) => {
71
- activeAction = action
72
-
73
- const statusDefault = document.querySelector('#status-default')
74
- const statusFailure = document.querySelector('#status-failure')
75
- const statusComputing = document.querySelector('#status-computing')
76
-
77
- const actions = {
78
- checkbox: document.querySelector('#checkbox'),
79
- failure: document.querySelector('#failure'),
80
- completed: document.querySelector('#completed'),
81
- loading: document.querySelector('#loading'),
82
- }
83
-
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')
96
-
97
- // Change the status text
98
- statusDefault?.classList.add('hidden')
99
- statusFailure?.classList.add('hidden')
100
- statusComputing?.classList.add('hidden')
101
-
102
- if (action === 'failure') {
103
- statusFailure?.classList.remove('hidden')
104
- } else if (action === 'loading') {
105
- statusComputing?.classList.remove('hidden')
106
- } else {
107
- statusDefault?.classList.remove('hidden')
108
- }
109
-
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
- }
117
- }
118
-
119
- const setLifetimeTimeout = () => {
120
- setTimeout(() => {
121
- sendMessageToLoader(IFRAME_MESSAGE_TYPES.TOKEN_EXPIRED)
122
- activateAction(ACTION.checkbox)
123
- }, CAPTCHA_TOKEN_LIFETIME * 1000)
124
- }
125
-
126
- const generateChallenge = async (): Promise<PowChallenge | null> => {
127
- try {
128
- const response = await fetch(`${API_URL}${ENDPOINTS.GENERATE}`, {
129
- method: 'POST',
130
- headers: {
131
- 'Content-Type': 'application/json',
132
- },
133
- body: JSON.stringify({
134
- // @ts-ignore
135
- pid: window.__SWETRIX_PROJECT_ID,
136
- }),
137
- })
138
-
139
- if (!response.ok) {
140
- throw new Error('Failed to generate challenge')
141
- }
142
-
143
- return await response.json()
144
- } catch (e) {
145
- sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
146
- activateAction(ACTION.failure)
147
- return null
148
- }
149
- }
150
-
151
- const verifySolution = async (challenge: string, nonce: number, solution: string): Promise<string | null> => {
152
- try {
153
- const response = await fetch(`${API_URL}${ENDPOINTS.VERIFY}`, {
154
- method: 'POST',
155
- headers: {
156
- 'Content-Type': 'application/json',
157
- },
158
- body: JSON.stringify({
159
- challenge,
160
- nonce,
161
- solution,
162
- // @ts-ignore
163
- pid: window.__SWETRIX_PROJECT_ID,
164
- }),
165
- })
166
-
167
- if (!response.ok) {
168
- throw new Error('Verification failed')
169
- }
170
-
171
- const data = await response.json()
172
-
173
- if (!data.success) {
174
- throw new Error('Verification failed')
175
- }
176
-
177
- return data.token
178
- } catch (e) {
179
- sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
180
- activateAction(ACTION.failure)
181
- return null
182
- }
183
- }
184
-
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
- }
191
-
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)
197
- return
198
- }
199
-
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
206
-
207
- if (data.type === 'progress') {
208
- return
209
- }
210
-
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
- }
221
-
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)
252
- sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
253
- activateAction(ACTION.failure)
254
- powWorker?.terminate()
255
- powWorker = null
256
- resolve()
257
- }
258
-
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`)
302
- sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
303
- activateAction(ACTION.failure)
304
- return
305
- }
306
-
307
- const input = `${challengeStr}:${nonce}`
308
- const hash = await sha256(input)
309
-
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
- }
318
- return
319
- }
320
-
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')
333
-
334
- branding?.addEventListener('click', (e: Event) => {
335
- e.stopPropagation()
336
- })
337
-
338
- captchaComponent?.addEventListener('click', async () => {
339
- if (activeAction === ACTION.loading || activeAction === ACTION.completed) {
340
- return
341
- }
342
-
343
- if (activeAction === ACTION.failure) {
344
- activateAction(ACTION.checkbox)
345
- return
346
- }
347
-
348
- activateAction(ACTION.loading)
349
-
350
- const challenge = await generateChallenge()
351
-
352
- if (!challenge) {
353
- return
354
- }
355
-
356
- await solveChallenge(challenge)
357
- })
358
- })
@@ -1,284 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="utf-8">
6
- <title>Swetrix Captcha</title>
7
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
8
-
9
- <style>
10
- html {
11
- font-family: '-apple-system', 'system-ui', 'BlinkMacSystemFont', 'Inter', 'Cantarell', 'Helvetica Neue', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif;
12
- -webkit-font-smoothing: antialiased;
13
- -moz-osx-font-smoothing: grayscale;
14
- }
15
-
16
- body {
17
- margin: 0;
18
- padding: 0;
19
- height: auto;
20
- overflow: auto;
21
- }
22
-
23
- #swetrix-captcha {
24
- display: flex;
25
- justify-content: center;
26
- align-items: center;
27
- position: relative;
28
-
29
- /* 300 - 20px (padding) */
30
- width: 280px;
31
-
32
- background-color: #0f172a;
33
- border: 1px solid #1e293b;
34
- height: 64px;
35
- -webkit-user-select: none;
36
- user-select: none;
37
- padding-left: 10px;
38
- padding-right: 10px;
39
-
40
- cursor: pointer;
41
- }
42
-
43
- #swetrix-captcha:hover {
44
- background-color: #1e293b;
45
- }
46
-
47
- #challenge {
48
- display: flex;
49
- align-items: center;
50
- cursor: pointer;
51
- flex: 1;
52
- }
53
-
54
- #action {
55
- margin-right: 10px;
56
- position: relative;
57
- width: 28px;
58
- height: 28px;
59
- }
60
-
61
- #checkbox {
62
- background-color: #111827;
63
- border: 1px solid #374151;
64
- height: 25px;
65
- width: 25px;
66
- border-radius: 3px;
67
- position: absolute;
68
- top: 50%;
69
- left: 50%;
70
- transform: translate(-50%, -50%);
71
- transition: opacity 0.25s ease, transform 0.25s ease;
72
- }
73
-
74
- #checkbox:hover {
75
- border-color: #4b5563;
76
- }
77
-
78
- #status {
79
- font-size: 14px;
80
- color: #f9fafb;
81
- }
82
-
83
- #status span {
84
- transition: opacity 0.2s ease;
85
- }
86
-
87
- #status-computing {
88
- display: flex;
89
- flex-direction: column;
90
- }
91
-
92
- .hidden {
93
- display: none !important;
94
- }
95
-
96
- /* Fade out animation for hiding elements */
97
- .fade-out {
98
- opacity: 0 !important;
99
- transform: translate(-50%, -50%) scale(0.8) !important;
100
- pointer-events: none;
101
- }
102
-
103
- /* Fade in animation for showing elements */
104
- .fade-in {
105
- opacity: 1;
106
- transform: translate(-50%, -50%) scale(1);
107
- }
108
-
109
- #branding {
110
- position: absolute;
111
- bottom: 4px;
112
- right: 10px;
113
- }
114
-
115
- #branding a {
116
- font-size: 9px;
117
- color: #6b7280;
118
- text-decoration: none;
119
- transition: color 0.2s ease;
120
- }
121
-
122
- #branding a:hover {
123
- color: #9ca3af;
124
- text-decoration: underline;
125
- }
126
-
127
- #action svg {
128
- width: 28px;
129
- height: 28px;
130
- }
131
-
132
- #failure > svg {
133
- /* bg-red-500 */
134
- color: #d6292a;
135
- }
136
-
137
- #completed > svg {
138
- /* bg-green-600 */
139
- color: #16a24c;
140
- }
141
-
142
- #completed,
143
- #failure {
144
- display: flex;
145
- align-items: center;
146
- justify-content: center;
147
- position: absolute;
148
- top: 50%;
149
- left: 50%;
150
- transform: translate(-50%, -50%) scale(0.8);
151
- opacity: 0;
152
- transition: opacity 0.3s ease, transform 0.3s ease;
153
- }
154
-
155
- #completed.show,
156
- #failure.show {
157
- opacity: 1;
158
- transform: translate(-50%, -50%) scale(1);
159
- }
160
-
161
- /* Checkmark draw animation */
162
- #completed.show svg path {
163
- stroke-dasharray: 24;
164
- stroke-dashoffset: 24;
165
- animation: drawCheck 0.4s ease forwards 0.1s;
166
- }
167
-
168
- @keyframes drawCheck {
169
- to {
170
- stroke-dashoffset: 0;
171
- }
172
- }
173
-
174
- /* Failure shake animation */
175
- #failure.show {
176
- animation: shake 0.4s ease;
177
- }
178
-
179
- @keyframes shake {
180
- 0%, 100% { transform: translate(-50%, -50%) scale(1) rotate(0deg); }
181
- 25% { transform: translate(-50%, -50%) scale(1) rotate(-5deg); }
182
- 75% { transform: translate(-50%, -50%) scale(1) rotate(5deg); }
183
- }
184
-
185
- /* Loading indicator - Material Design style spinner */
186
- #loading {
187
- position: absolute;
188
- top: 50%;
189
- left: 50%;
190
- transform: translate(-50%, -50%);
191
- width: 24px;
192
- height: 24px;
193
- opacity: 0;
194
- transition: opacity 0.25s ease;
195
- }
196
-
197
- #loading.show {
198
- opacity: 1;
199
- }
200
-
201
- #loading svg {
202
- width: 24px;
203
- height: 24px;
204
- animation: rotate 1.4s linear infinite;
205
- }
206
-
207
- #loading svg circle {
208
- stroke: #60a5fa;
209
- stroke-linecap: round;
210
- animation: dash 1.4s ease-in-out infinite;
211
- }
212
-
213
- @keyframes rotate {
214
- 100% {
215
- transform: rotate(360deg);
216
- }
217
- }
218
-
219
- @keyframes dash {
220
- 0% {
221
- stroke-dasharray: 1, 150;
222
- stroke-dashoffset: 0;
223
- }
224
- 50% {
225
- stroke-dasharray: 90, 150;
226
- stroke-dashoffset: -35;
227
- }
228
- 100% {
229
- stroke-dasharray: 90, 150;
230
- stroke-dashoffset: -124;
231
- }
232
- }
233
- </style>
234
- <script>
235
- window.__SWETRIX_CAPTCHA_THEME = 'dark'
236
- const urlParams = new URLSearchParams(window.location.search)
237
- const pid = urlParams.get('pid')
238
- const cid = urlParams.get('cid')
239
-
240
- window.__SWETRIX_CAPTCHA_ID = cid
241
- window.__SWETRIX_PROJECT_ID = pid
242
- </script>
243
- <script src="../captcha.js" defer></script>
244
- </head>
245
-
246
- <body>
247
- <div id="swetrix-captcha">
248
- <div id="challenge">
249
- <div id="action">
250
- <!-- Can contain a checkbox itself / a red cross (with a retry action) / a green check mark -->
251
- <div id="checkbox"></div>
252
- <div id="failure">
253
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
254
- <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/>
255
- <path d="M12 9v4"/>
256
- <path d="M12 17h.01"/>
257
- </svg>
258
- </div>
259
- <div id="completed">
260
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
261
- <path d="M20 6 9 17l-5-5"/>
262
- </svg>
263
- </div>
264
- <div id="loading">
265
- <svg viewBox="0 0 50 50">
266
- <circle cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
267
- </svg>
268
- </div>
269
- </div>
270
- <div id="status">
271
- <span id="status-default">I am human</span>
272
- <span id="status-failure" class="hidden">Failure, please retry</span>
273
- <span id="status-computing" class="hidden">
274
- <span>Verifying...</span>
275
- </span>
276
- </div>
277
- </div>
278
- <div id="branding">
279
- <a href="https://swetrix.com/captcha" target="_blank" rel="noopener noreferrer">Swetrix Captcha</a>
280
- </div>
281
- </div>
282
- </body>
283
-
284
- </html>