@maggidev/captchashield 1.0.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/LICENSE +21 -0
- package/README.md +307 -0
- package/dist/index.cjs +644 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +181 -0
- package/dist/index.d.ts +181 -0
- package/dist/index.mjs +611 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 N0tMaggi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="docs/assets/shield-mark.svg" alt="CaptchaShield mark" width="72" />
|
|
3
|
+
<h1>CaptchaShield</h1>
|
|
4
|
+
<p>Cloudflare Turnstile modal for browser applications with secure defaults, optional backend verification, and a dedicated local demo lab.</p>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/captchashield)
|
|
8
|
+
[](https://www.npmjs.com/package/captchashield)
|
|
9
|
+
[](https://bundlephobia.com/package/captchashield)
|
|
10
|
+
[](https://github.com/N0tMaggi/CapchaShield/actions/workflows/ci.yml)
|
|
11
|
+
[](#api-at-a-glance)
|
|
12
|
+
[](LICENSE)
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
CaptchaShield exists for teams that want a straightforward Turnstile integration without rebuilding the same modal, cleanup, retry, and verification flow for every project.
|
|
17
|
+
|
|
18
|
+
- It keeps the browser-side integration small and focused.
|
|
19
|
+
- It supports both the built-in modal and custom renderers.
|
|
20
|
+
- It treats client persistence as explicit opt-in instead of a hidden default.
|
|
21
|
+
- It makes local testing easier with a dedicated demo page and mock widget.
|
|
22
|
+
- It is designed to stay honest about what is UX and what must still be enforced on the backend.
|
|
23
|
+
|
|
24
|
+
## Technologies
|
|
25
|
+
|
|
26
|
+
- TypeScript for the package surface and internal logic
|
|
27
|
+
- Cloudflare Turnstile as the challenge provider
|
|
28
|
+
- `tsup` for package bundling
|
|
29
|
+
- `vitest` with `jsdom` for unit and behavior tests
|
|
30
|
+
- `eslint` for static linting
|
|
31
|
+
- a small Node HTTP server for the local demo lab
|
|
32
|
+
|
|
33
|
+
## Demo
|
|
34
|
+
|
|
35
|
+
> The visuals below are from the local demo page in [`demo/`](demo). They show one testing surface only.
|
|
36
|
+
> They do not represent every possible integration style, every renderer, or a recommended production design for all consumers of the package.
|
|
37
|
+
|
|
38
|
+
<p align="center">
|
|
39
|
+
<img src="docs/assets/demo-flow.gif" alt="Demo flow" width="860" />
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<p align="center">
|
|
43
|
+
<img src="docs/assets/demo-overview.png" alt="Demo overview" width="860" />
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<p align="center">
|
|
47
|
+
<img src="docs/assets/demo-modal.png" alt="Default modal screenshot" width="460" />
|
|
48
|
+
<img src="docs/assets/demo-custom.png" alt="Custom renderer screenshot" width="860" />
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
Run the local demo:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm run demo
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then open:
|
|
58
|
+
|
|
59
|
+
[http://127.0.0.1:4173/demo/](http://127.0.0.1:4173/demo/)
|
|
60
|
+
|
|
61
|
+
The demo page includes:
|
|
62
|
+
|
|
63
|
+
- default modal and custom renderer flows
|
|
64
|
+
- local mock Turnstile behavior
|
|
65
|
+
- local verify and status endpoints
|
|
66
|
+
- session-only versus trusted-cookie behavior
|
|
67
|
+
- tamper simulation
|
|
68
|
+
- live config preview and event log
|
|
69
|
+
|
|
70
|
+
## How It Works
|
|
71
|
+
|
|
72
|
+
### Verification Flow
|
|
73
|
+
|
|
74
|
+
```mermaid
|
|
75
|
+
sequenceDiagram
|
|
76
|
+
participant User
|
|
77
|
+
participant App
|
|
78
|
+
participant CaptchaShield
|
|
79
|
+
participant Turnstile
|
|
80
|
+
participant Backend
|
|
81
|
+
|
|
82
|
+
User->>App: Request protected action
|
|
83
|
+
App->>CaptchaShield: open()
|
|
84
|
+
alt already verified in session or trusted cookie
|
|
85
|
+
CaptchaShield-->>App: already-verified
|
|
86
|
+
else needs challenge
|
|
87
|
+
CaptchaShield->>Turnstile: load script and render widget
|
|
88
|
+
Turnstile-->>CaptchaShield: token
|
|
89
|
+
CaptchaShield->>Backend: POST verify(token)
|
|
90
|
+
alt accepted
|
|
91
|
+
Backend-->>CaptchaShield: 2xx
|
|
92
|
+
CaptchaShield-->>App: rendered / verified
|
|
93
|
+
else rejected
|
|
94
|
+
Backend-->>CaptchaShield: non-2xx
|
|
95
|
+
CaptchaShield->>Turnstile: reset widget
|
|
96
|
+
CaptchaShield-->>App: onError(...)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Package Responsibilities
|
|
102
|
+
|
|
103
|
+
```mermaid
|
|
104
|
+
flowchart LR
|
|
105
|
+
A["App code"] --> B["createCaptchaShield(config)"]
|
|
106
|
+
B --> C["Script loading and validation"]
|
|
107
|
+
B --> D["Modal or custom renderer"]
|
|
108
|
+
B --> E["Verification request handling"]
|
|
109
|
+
B --> F["Session and optional trusted cookie state"]
|
|
110
|
+
B --> G["Lifecycle cleanup and reset"]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Demo Lab Surface
|
|
114
|
+
|
|
115
|
+
```mermaid
|
|
116
|
+
flowchart TB
|
|
117
|
+
UI["demo/index.html"] --> APP["demo/app.js"]
|
|
118
|
+
APP --> LIB["dist/index.mjs"]
|
|
119
|
+
APP --> MOCK["Mock Turnstile widget"]
|
|
120
|
+
APP --> STATUS["/api/status"]
|
|
121
|
+
APP --> VERIFY["/api/verify"]
|
|
122
|
+
STATUS --> SERVER["scripts/serve-demo.mjs"]
|
|
123
|
+
VERIFY --> SERVER
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Verified State
|
|
127
|
+
|
|
128
|
+
```mermaid
|
|
129
|
+
stateDiagram-v2
|
|
130
|
+
[*] --> Unverified
|
|
131
|
+
Unverified --> SessionVerified: verify success
|
|
132
|
+
SessionVerified --> Unverified: reset() or destroy()
|
|
133
|
+
SessionVerified --> TrustedCookieVerified: trustClientCookie enabled
|
|
134
|
+
TrustedCookieVerified --> Unverified: cookie cleared or expired
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## What The Package Handles
|
|
138
|
+
|
|
139
|
+
- Turnstile script loading from the official Cloudflare host
|
|
140
|
+
- built-in modal rendering with sane defaults
|
|
141
|
+
- custom render hook support
|
|
142
|
+
- token verification requests with timeout and retry handling
|
|
143
|
+
- widget reset and cleanup after reject or error
|
|
144
|
+
- optional status precheck before rendering
|
|
145
|
+
- challenge presence and removal monitoring
|
|
146
|
+
- typed error callbacks
|
|
147
|
+
|
|
148
|
+
## What Your Backend Still Must Handle
|
|
149
|
+
|
|
150
|
+
- final authorization decisions
|
|
151
|
+
- Turnstile secret management
|
|
152
|
+
- token verification against Cloudflare `siteverify`
|
|
153
|
+
- route protection and abuse policy
|
|
154
|
+
- rate limiting, IP policy, and application-specific trust decisions
|
|
155
|
+
|
|
156
|
+
## Install
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npm install captchashield
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Quick Start
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { createCaptchaShield } from 'captchashield';
|
|
166
|
+
|
|
167
|
+
const shield = createCaptchaShield({
|
|
168
|
+
siteKey: '<your-turnstile-sitekey>',
|
|
169
|
+
verify: {
|
|
170
|
+
endpoint: '/api/turnstile/verify',
|
|
171
|
+
},
|
|
172
|
+
onVerified: (token) => {
|
|
173
|
+
console.log('Verified token', token);
|
|
174
|
+
},
|
|
175
|
+
onError: (error) => {
|
|
176
|
+
console.error(error.message);
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await shield.open();
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
By default, verified state is session-local. Persistent skip via cookie only happens when `cookie.trustClientCookie` is enabled.
|
|
184
|
+
|
|
185
|
+
## Security Model
|
|
186
|
+
|
|
187
|
+
CaptchaShield improves Turnstile UX. It is not a substitute for backend enforcement.
|
|
188
|
+
|
|
189
|
+
- Always verify Turnstile tokens on your server for protected actions.
|
|
190
|
+
- Treat client cookies as UX only. Do not use them as authorization.
|
|
191
|
+
- Only enable `cookie.trustClientCookie` when client-side skip is acceptable for your use case.
|
|
192
|
+
- Verification only supports `POST`, so tokens do not end up in URLs.
|
|
193
|
+
- Endpoint configuration is validated and custom script loading is restricted to the official Cloudflare host.
|
|
194
|
+
- `cookie.name`, `cookie.domain`, and `cookie.path` are validated against RFC 6265 at construction time — passing values with semicolons or control characters throws immediately.
|
|
195
|
+
- Custom CSS is injected as-is; never pass user-generated CSS into `modal.styles.customCss`.
|
|
196
|
+
|
|
197
|
+
## Common Config
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const shield = createCaptchaShield({
|
|
201
|
+
siteKey: '0x4AAAAAA...',
|
|
202
|
+
cookie: {
|
|
203
|
+
secure: true,
|
|
204
|
+
sameSite: 'Strict',
|
|
205
|
+
trustClientCookie: false,
|
|
206
|
+
},
|
|
207
|
+
integrity: {
|
|
208
|
+
verifyTurnstileGlobal: true,
|
|
209
|
+
enforceChallengePresence: true,
|
|
210
|
+
monitorChallengeRemoval: true,
|
|
211
|
+
},
|
|
212
|
+
verify: {
|
|
213
|
+
endpoint: '/api/security/verify-captcha',
|
|
214
|
+
timeoutMs: 5000,
|
|
215
|
+
retries: 1,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Custom Renderer
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
createCaptchaShield({
|
|
224
|
+
siteKey: '<sitekey>',
|
|
225
|
+
render: ({ challengeContainer, close }) => {
|
|
226
|
+
const root = document.createElement('div');
|
|
227
|
+
const panel = document.createElement('section');
|
|
228
|
+
const heading = document.createElement('h2');
|
|
229
|
+
const closeButton = document.createElement('button');
|
|
230
|
+
|
|
231
|
+
heading.textContent = 'Verification required';
|
|
232
|
+
closeButton.textContent = 'Close';
|
|
233
|
+
closeButton.onclick = close;
|
|
234
|
+
|
|
235
|
+
panel.append(heading, challengeContainer, closeButton);
|
|
236
|
+
root.append(panel);
|
|
237
|
+
|
|
238
|
+
return { root };
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Minimal Backend Example
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import type { Request, Response } from 'express';
|
|
247
|
+
import fetch from 'node-fetch';
|
|
248
|
+
|
|
249
|
+
const TURNSTILE_SECRET = process.env.TURNSTILE_SECRET!;
|
|
250
|
+
|
|
251
|
+
export async function verifyTurnstile(req: Request, res: Response) {
|
|
252
|
+
const token = req.body?.token;
|
|
253
|
+
if (!token) return res.status(400).json({ error: 'missing token' });
|
|
254
|
+
|
|
255
|
+
const cfRes = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
|
256
|
+
method: 'POST',
|
|
257
|
+
headers: { 'content-type': 'application/json' },
|
|
258
|
+
body: JSON.stringify({ secret: TURNSTILE_SECRET, response: token }),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const payload = await cfRes.json();
|
|
262
|
+
if (payload.success) return res.sendStatus(204);
|
|
263
|
+
return res.status(400).json({ success: false, error: payload['error-codes'] });
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## API At A Glance
|
|
268
|
+
|
|
269
|
+
`createCaptchaShield(config)` returns:
|
|
270
|
+
|
|
271
|
+
- `open(): Promise<{ status: 'rendered' | 'already-verified'; reason?: 'cookie' | 'session' }>`
|
|
272
|
+
- `close()`: remove the modal without clearing state
|
|
273
|
+
- `reset()`: clear token, trusted cookie, and reset the widget
|
|
274
|
+
- `destroy()`: reset and close
|
|
275
|
+
- `isVerified()`: inspect current verified state
|
|
276
|
+
- `getToken()`: read the last token seen by the instance
|
|
277
|
+
|
|
278
|
+
Main config areas:
|
|
279
|
+
|
|
280
|
+
- `modal`: copy, classes, default style injection, custom CSS
|
|
281
|
+
- `cookie`: name, scope, lifetime, SameSite, secure flag, `trustClientCookie`
|
|
282
|
+
- `verify`: backend endpoint, timeout, retries, headers, expected status
|
|
283
|
+
- `statusCheck`: optional preflight request before render
|
|
284
|
+
- `integrity`: global checks, challenge presence enforcement, removal monitoring
|
|
285
|
+
- `render`: custom renderer hook
|
|
286
|
+
|
|
287
|
+
## Scripts
|
|
288
|
+
|
|
289
|
+
- `npm run dev` - tsup watch
|
|
290
|
+
- `npm run build` - build ESM, CJS, and type declarations
|
|
291
|
+
- `npm run test` - Vitest
|
|
292
|
+
- `npm run lint` - ESLint
|
|
293
|
+
- `npm run typecheck` - TypeScript no-emit check
|
|
294
|
+
- `npm run demo` - build and start the local demo page
|
|
295
|
+
- `npm run demo:serve` - start the demo server without rebuilding
|
|
296
|
+
|
|
297
|
+
## Roadmap
|
|
298
|
+
|
|
299
|
+
- Signed skip tokens backed by the server instead of plain trusted cookies
|
|
300
|
+
- First-party renderer presets for inline, sheet, and compact verification UIs
|
|
301
|
+
- Better analytics hooks for render, verify success, reject, timeout, and tamper events
|
|
302
|
+
- Framework adapters for Next.js, Express, edge runtimes, and Laravel
|
|
303
|
+
- A small end-to-end browser test suite for demo and package regression checks
|
|
304
|
+
|
|
305
|
+
## License
|
|
306
|
+
|
|
307
|
+
MIT
|