@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 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
+ [![npm version](https://img.shields.io/npm/v/captchashield?color=6b4f3a&label=npm)](https://www.npmjs.com/package/captchashield)
8
+ [![npm downloads](https://img.shields.io/npm/dw/captchashield?color=8b6a4d)](https://www.npmjs.com/package/captchashield)
9
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/captchashield?label=min%2Bgzip&color=5b4636)](https://bundlephobia.com/package/captchashield)
10
+ [![CI](https://github.com/N0tMaggi/CapchaShield/actions/workflows/ci.yml/badge.svg)](https://github.com/N0tMaggi/CapchaShield/actions/workflows/ci.yml)
11
+ [![types](https://img.shields.io/badge/TypeScript-ready-6b4f3a)](#api-at-a-glance)
12
+ [![license](https://img.shields.io/badge/License-MIT-7f674f)](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