@swetrix/captcha 1.0.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.
@@ -0,0 +1,242 @@
1
+ // @ts-ignore
2
+ const isDevelopment = window.__SWETRIX_CAPTCHA_DEV || false
3
+
4
+ const CAPTCHA_SELECTOR = '.swecaptcha'
5
+ const LIGHT_CAPTCHA_IFRAME_URL = isDevelopment ? './light.html' : 'https://cap.swetrix.com/pages/light'
6
+ const DARK_CAPTCHA_IFRAME_URL = isDevelopment ? './dark.html' : 'https://cap.swetrix.com/pages/dark'
7
+ const DEFAULT_RESPONSE_INPUT_NAME = 'swetrix-captcha-response'
8
+ const MESSAGE_IDENTIFIER = 'swetrix-captcha'
9
+ const ID_PREFIX = 'swetrix-captcha-'
10
+ const THEMES = ['light', 'dark']
11
+ const PID_REGEX = /^(?!.*--)[a-zA-Z0-9-]{12}$/
12
+
13
+ enum LOG_ACTIONS {
14
+ log = 'log',
15
+ error = 'error',
16
+ warn = 'warn',
17
+ info = 'info',
18
+ }
19
+
20
+ const DUMMY_PIDS = [
21
+ 'AP00000000000', 'MP00000000000', 'FAIL000000000',
22
+ ]
23
+
24
+ const isValidPID = (pid: string) => DUMMY_PIDS.includes(pid) || PID_REGEX.test(pid)
25
+
26
+ const FRAME_HEIGHT_MAPPING = {
27
+ default: '66px',
28
+ manual: '200px',
29
+ }
30
+
31
+ const getFrameID = (cid: string) => `${cid}-frame`
32
+
33
+ const ids: string[] = []
34
+
35
+ const log = (status: LOG_ACTIONS, text: string) => {
36
+ console[status](`[Swetrix Captcha] ${text}`)
37
+ }
38
+
39
+ const appendParamsToURL = (url: string, params: any) => {
40
+ const queryString = Object.keys(params).map((key) => {
41
+ return `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`
42
+ }).join('&')
43
+
44
+ return `${url}?${queryString}`
45
+ }
46
+
47
+ const renderCaptcha = (container: Element, params: any) => {
48
+ const cid = generateRandomID()
49
+ const cParams = {
50
+ ...params,
51
+ cid, // CAPTCHA ID
52
+ }
53
+
54
+ const frame = generateCaptchaFrame(cParams)
55
+ const input = generateHiddenInput(cParams)
56
+
57
+ container.appendChild(frame)
58
+ container.appendChild(input)
59
+ }
60
+
61
+ const generateRandomID = (): string => {
62
+ const randomID = ID_PREFIX + Math.random().toString(36).substr(2, 6)
63
+
64
+ if (ids.includes(randomID)) {
65
+ return generateRandomID()
66
+ }
67
+
68
+ ids.push(randomID)
69
+
70
+ return randomID
71
+ }
72
+
73
+ const postMessageCallback = (pmEvent: MessageEvent) => {
74
+ // TODO: Validate origin
75
+
76
+ const { data } = pmEvent
77
+
78
+ if (!data) {
79
+ return
80
+ }
81
+
82
+ const {
83
+ type, cid, event,
84
+ } = data
85
+
86
+ if (type !== MESSAGE_IDENTIFIER) {
87
+ return
88
+ }
89
+
90
+ if (!cid || !ids.includes(cid)) {
91
+ return
92
+ }
93
+
94
+ const input = document.getElementById(cid)
95
+ const inputExists = input !== null
96
+
97
+ switch (event) {
98
+ case 'success': {
99
+ const { token } = data
100
+
101
+ if (!inputExists) {
102
+ log(LOG_ACTIONS.error, '[PM -> success] Input element does not exist.')
103
+ return
104
+ }
105
+
106
+ // @ts-ignore
107
+ input.value = token
108
+
109
+ break
110
+ }
111
+
112
+ case 'failure': {
113
+ if (!inputExists) {
114
+ log(LOG_ACTIONS.error, '[PM -> failure] Input element does not exist.')
115
+ return
116
+ }
117
+
118
+ // @ts-ignore
119
+ input.value = ''
120
+
121
+ break
122
+ }
123
+
124
+ case 'tokenExpired': {
125
+ if (!inputExists) {
126
+ log(LOG_ACTIONS.error, '[PM -> failure] Input element does not exist.')
127
+ return
128
+ }
129
+
130
+ // @ts-ignore
131
+ input.value = ''
132
+
133
+ break
134
+ }
135
+
136
+ case 'manualStarted': {
137
+ const frame = document.getElementById(getFrameID(cid))
138
+
139
+ if (!frame) {
140
+ log(LOG_ACTIONS.error, '[PM -> manualStarted] Frame does not exist.')
141
+ return
142
+ }
143
+
144
+ frame.style.height = FRAME_HEIGHT_MAPPING.manual
145
+
146
+ break
147
+ }
148
+
149
+ case 'manualFinished': {
150
+ const frame = document.getElementById(getFrameID(cid))
151
+
152
+ if (!frame) {
153
+ log(LOG_ACTIONS.error, '[PM -> manualFinished] Frame does not exist.')
154
+ return
155
+ }
156
+
157
+ frame.style.height = FRAME_HEIGHT_MAPPING.default
158
+
159
+ break
160
+ }
161
+ }
162
+ }
163
+
164
+ const generateCaptchaFrame = (params: any) => {
165
+ const { theme } = params
166
+ const captchaFrame = document.createElement('iframe')
167
+
168
+ captchaFrame.id = getFrameID(params.cid)
169
+ captchaFrame.src = theme === 'dark'
170
+ ? appendParamsToURL(DARK_CAPTCHA_IFRAME_URL, params)
171
+ : appendParamsToURL(LIGHT_CAPTCHA_IFRAME_URL, params)
172
+ captchaFrame.style.height = FRAME_HEIGHT_MAPPING.default
173
+ captchaFrame.title = 'Swetrix Captcha'
174
+ captchaFrame.style.border = 'none'
175
+ captchaFrame.style.width = '302px'
176
+ captchaFrame.style.overflow = 'visible'
177
+
178
+ return captchaFrame
179
+ }
180
+
181
+ const generateHiddenInput = (params: any) => {
182
+ const { cid } = params
183
+ const input = document.createElement('input')
184
+
185
+ input.type = 'hidden'
186
+ input.name = params.respName
187
+ input.value = ''
188
+ input.id = cid
189
+
190
+ return input
191
+ }
192
+
193
+ const validateParams = (params: any) => {
194
+ const { theme, pid } = params
195
+
196
+ if (theme && !THEMES.includes(theme)) {
197
+ log(LOG_ACTIONS.error, `Invalid data-theme parameter: ${theme}`)
198
+ return false
199
+ }
200
+
201
+ if (!pid || !isValidPID(pid)) {
202
+ log(LOG_ACTIONS.error, `Invalid data-project-id parameter: ${pid}`)
203
+ return false
204
+ }
205
+
206
+ return true
207
+ }
208
+
209
+ const parseParams = (container: Element): object => ({
210
+ pid: container.getAttribute('data-project-id'),
211
+ respName: container.getAttribute('data-response-input-name') || DEFAULT_RESPONSE_INPUT_NAME,
212
+ theme: container.getAttribute('data-theme'),
213
+ })
214
+
215
+ const main = (forced = false) => {
216
+ if (!forced && 'swecaptcha' in window) {
217
+ log(LOG_ACTIONS.warn, 'Captcha is already loaded.')
218
+ }
219
+
220
+ // TODO: Add some callbacks here
221
+ // @ts-ignore
222
+ window.swecaptcha = true
223
+ window.addEventListener('message', postMessageCallback)
224
+
225
+ const containers = Array.from(document.querySelectorAll(CAPTCHA_SELECTOR))
226
+
227
+ for (const container of containers) {
228
+ const params = parseParams(container)
229
+
230
+ if (!validateParams(params)) {
231
+ log(LOG_ACTIONS.error, 'Aborting captcha rendering due to invalid parameters.')
232
+ return
233
+ }
234
+
235
+ renderCaptcha(container, params)
236
+ }
237
+ }
238
+
239
+ // @ts-ignore
240
+ window.swetrixCaptchaForceLoad = () => main(true)
241
+
242
+ document.addEventListener('DOMContentLoaded', () => main())
package/src/captcha.ts ADDED
@@ -0,0 +1,284 @@
1
+ // @ts-ignore
2
+ const isDevelopment = window.__SWETRIX_CAPTCHA_DEV || false
3
+
4
+ const API_URL = isDevelopment ? 'http://localhost:5005/v1/captcha' : 'https://api.swetrix.com/v1/captcha'
5
+ const MSG_IDENTIFIER = 'swetrix-captcha'
6
+ const DEFAULT_THEME = 'light'
7
+ const CAPTCHA_TOKEN_LIFETIME = 300 // seconds (5 minutes).
8
+ let TOKEN = ''
9
+ let HASH = ''
10
+
11
+ const ENDPOINTS = {
12
+ VERIFY: '/verify',
13
+ GENERATE: '/generate',
14
+ VERIFY_MANUAL: '/verify-manual',
15
+ }
16
+
17
+ enum IFRAME_MESSAGE_TYPES {
18
+ SUCCESS = 'success',
19
+ FAILURE = 'failure',
20
+ TOKEN_EXPIRED = 'tokenExpired',
21
+ MANUAL_STARTED = 'manualStarted',
22
+ MANUAL_FINISHED = 'manualFinished',
23
+ }
24
+
25
+ enum ACTION {
26
+ checkbox = 'checkbox',
27
+ failure = 'failure',
28
+ completed = 'completed',
29
+ loading = 'loading',
30
+ }
31
+
32
+ let activeAction: ACTION = ACTION.checkbox
33
+
34
+ const sendMessageToLoader = (event: IFRAME_MESSAGE_TYPES, data = {}) => {
35
+ window.parent.postMessage({
36
+ event,
37
+ type: MSG_IDENTIFIER,
38
+ // @ts-ignore
39
+ cid: window.__SWETRIX_CAPTCHA_ID,
40
+ ...data,
41
+ }, '*')
42
+ }
43
+
44
+ /**
45
+ * Sets the provided action visible and the rest hidden
46
+ * @param {*} action checkbox | failure | completed | loading
47
+ */
48
+ const activateAction = (action: ACTION) => {
49
+ activeAction = action
50
+
51
+ const statusDefault = document.querySelector('#status-default')
52
+ const statusFailure = document.querySelector('#status-failure')
53
+
54
+ const actions = {
55
+ checkbox: document.querySelector('#checkbox'),
56
+ failure: document.querySelector('#failure'),
57
+ completed: document.querySelector('#completed'),
58
+ loading: document.querySelector('#loading'),
59
+ }
60
+
61
+ // Apply hidden class to all actions
62
+ actions.checkbox?.classList.add('hidden')
63
+ actions.failure?.classList.add('hidden')
64
+ actions.completed?.classList.add('hidden')
65
+ actions.loading?.classList.add('hidden')
66
+
67
+ // Change the status text
68
+ if (action === 'failure') {
69
+ statusDefault?.classList.add('hidden')
70
+ statusFailure?.classList.remove('hidden')
71
+ } else {
72
+ statusDefault?.classList.remove('hidden')
73
+ statusFailure?.classList.add('hidden')
74
+ }
75
+
76
+ // Remove hidden class from the provided action
77
+ actions[action]?.classList.remove('hidden')
78
+ }
79
+
80
+ const setLifetimeTimeout = () => {
81
+ setTimeout(() => {
82
+ sendMessageToLoader(IFRAME_MESSAGE_TYPES.TOKEN_EXPIRED)
83
+ activateAction(ACTION.checkbox)
84
+ }, CAPTCHA_TOKEN_LIFETIME * 1000)
85
+ }
86
+
87
+ const enableManualChallenge = (svg: string) => {
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 () => {
122
+ try {
123
+ const response = await fetch(`${API_URL}${ENDPOINTS.GENERATE}`, {
124
+ method: 'POST',
125
+ headers: {
126
+ 'Content-Type': 'application/json',
127
+ },
128
+ body: JSON.stringify({
129
+ // @ts-ignore
130
+ theme: window.__SWETRIX_CAPTCHA_THEME || DEFAULT_THEME,
131
+ // @ts-ignore
132
+ pid: window.__SWETRIX_PROJECT_ID,
133
+ }),
134
+ })
135
+
136
+ if (!response.ok) {
137
+ throw ''
138
+ }
139
+
140
+ const data = await response.json()
141
+ return data
142
+ } catch (e) {
143
+ sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
144
+ activateAction(ACTION.failure)
145
+ return {}
146
+ }
147
+ }
148
+
149
+ const verify = async () => {
150
+ try {
151
+ const response = await fetch(`${API_URL}${ENDPOINTS.VERIFY}`, {
152
+ method: 'POST',
153
+ headers: {
154
+ 'Content-Type': 'application/json',
155
+ },
156
+ body: JSON.stringify({
157
+ // @ts-ignore
158
+ pid: window.__SWETRIX_PROJECT_ID,
159
+ }),
160
+ })
161
+
162
+ if (!response.ok) {
163
+ return {}
164
+ }
165
+
166
+ const data = await response.json()
167
+ return data
168
+ } catch (e) {
169
+ sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
170
+ activateAction(ACTION.failure)
171
+ return {}
172
+ }
173
+ }
174
+
175
+ document.addEventListener('DOMContentLoaded', () => {
176
+ const captchaComponent = document.querySelector('#swetrix-captcha')
177
+ const branding = document.querySelector('#branding')
178
+ const svgCaptchaInput = document.querySelector('#svg-captcha-input')
179
+ const manualSubmitBtn = document.querySelector('#manual-submit-btn')
180
+
181
+ branding?.addEventListener('click', (e: Event) => {
182
+ e.stopPropagation()
183
+ })
184
+
185
+ manualSubmitBtn?.addEventListener('click', async (e: Event) => {
186
+ e.stopPropagation()
187
+
188
+ if (!svgCaptchaInput) {
189
+ return
190
+ }
191
+
192
+ // @ts-ignore
193
+ const code = svgCaptchaInput.value
194
+
195
+ if (!code) {
196
+ return
197
+ }
198
+
199
+ let response
200
+
201
+ try {
202
+ response = await fetch(`${API_URL}${ENDPOINTS.VERIFY_MANUAL}`, {
203
+ method: 'POST',
204
+ body: JSON.stringify({
205
+ hash: HASH,
206
+ code,
207
+ // @ts-ignore
208
+ pid: window.__SWETRIX_PROJECT_ID,
209
+ }),
210
+ headers: {
211
+ 'Content-Type': 'application/json',
212
+ },
213
+ })
214
+ } catch (e) {
215
+ disableManualChallenge()
216
+ sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
217
+ activateAction(ACTION.failure)
218
+ // @ts-ignore
219
+ svgCaptchaInput.value = ''
220
+ return
221
+ }
222
+
223
+ if (!response.ok) {
224
+ disableManualChallenge()
225
+ sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
226
+ activateAction(ACTION.failure)
227
+ // @ts-ignore
228
+ svgCaptchaInput.value = ''
229
+ return
230
+ }
231
+
232
+ const { success, token } = await response.json()
233
+
234
+ if (!success) {
235
+ disableManualChallenge()
236
+ sendMessageToLoader(IFRAME_MESSAGE_TYPES.FAILURE)
237
+ activateAction(ACTION.failure)
238
+ // @ts-ignore
239
+ svgCaptchaInput.value = ''
240
+ return
241
+ }
242
+
243
+ // @ts-ignore
244
+ svgCaptchaInput.value = ''
245
+
246
+ sendMessageToLoader(IFRAME_MESSAGE_TYPES.SUCCESS, { token })
247
+ setLifetimeTimeout()
248
+ activateAction(ACTION.completed)
249
+ disableManualChallenge()
250
+ })
251
+
252
+ captchaComponent?.addEventListener('click', async () => {
253
+ if (activeAction === ACTION.loading || activeAction === ACTION.completed) {
254
+ return
255
+ }
256
+
257
+ if (activeAction === ACTION.failure) {
258
+ activateAction(ACTION.checkbox)
259
+ return
260
+ }
261
+
262
+ activateAction(ACTION.loading)
263
+
264
+ try {
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()
278
+
279
+ HASH = hash
280
+ enableManualChallenge(data)
281
+ return
282
+ }
283
+ })
284
+ })