api-turnstile 0.1.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 +378 -0
- package/dist/client/sentinel.d.ts +20 -0
- package/dist/client/sentinel.d.ts.map +1 -0
- package/dist/client/sentinel.js +83 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/middleware/express.d.ts +7 -0
- package/dist/middleware/express.d.ts.map +1 -0
- package/dist/middleware/express.js +194 -0
- package/dist/middleware/fastify.d.ts +10 -0
- package/dist/middleware/fastify.d.ts.map +1 -0
- package/dist/middleware/fastify.js +76 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +27 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sentinel
|
|
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,378 @@
|
|
|
1
|
+
# 🛡️ api-turnstile
|
|
2
|
+
|
|
3
|
+
> **Cloudflare Turnstile protects browsers — not APIs.**
|
|
4
|
+
> **Sentinel is a Turnstile for APIs.**
|
|
5
|
+
> **Block bots, scripts, and automation without CAPTCHAs.**
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why This Exists
|
|
10
|
+
|
|
11
|
+
Your API endpoints are under constant attack from:
|
|
12
|
+
- ✅ Credential stuffing bots
|
|
13
|
+
- ✅ Automated account creation
|
|
14
|
+
- ✅ Payment fraud scripts
|
|
15
|
+
- ✅ API scrapers
|
|
16
|
+
|
|
17
|
+
Traditional solutions force you to choose between:
|
|
18
|
+
- ❌ **CAPTCHAs** (user-hostile, kills conversion)
|
|
19
|
+
- ❌ **Rate limiting** (blocks legitimate users during traffic spikes)
|
|
20
|
+
- ❌ **IP blocking** (trivial to bypass with proxies)
|
|
21
|
+
|
|
22
|
+
**api-turnstile** is different. It's **drop-in API armor** that makes trust decisions in under 50ms using infrastructure and behavioral signals.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install api-turnstile
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
Protect your signup endpoint in **under 60 seconds**:
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
import express from 'express';
|
|
40
|
+
import { sentinel } from 'api-turnstile';
|
|
41
|
+
|
|
42
|
+
const app = express();
|
|
43
|
+
|
|
44
|
+
app.use(sentinel({
|
|
45
|
+
apiKey: process.env.SENTINEL_KEY,
|
|
46
|
+
protect: ['/login', '/signup']
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
app.post('/signup', (req, res) => {
|
|
50
|
+
// Only legitimate traffic reaches here
|
|
51
|
+
res.json({ success: true });
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That's it. **92% of bot signups blocked automatically.**
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## How It Works
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
Client Request
|
|
63
|
+
↓
|
|
64
|
+
api-turnstile middleware
|
|
65
|
+
↓
|
|
66
|
+
Sentinel Decision Engine (< 50ms)
|
|
67
|
+
├─ ASN Analysis (Datacenter? VPN? Mobile?)
|
|
68
|
+
├─ Velocity Tracking (IP rotation? Burst traffic?)
|
|
69
|
+
└─ Behavioral Signals (Trust tokens? PoW?)
|
|
70
|
+
↓
|
|
71
|
+
PASS → Your handler
|
|
72
|
+
BLOCK → 403 Forbidden
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
No JavaScript required. No browser fingerprinting. **Pure API-native security.**
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
### Basic (Recommended)
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
app.use(sentinel({
|
|
85
|
+
apiKey: process.env.SENTINEL_KEY,
|
|
86
|
+
protect: ['/login', '/signup', '/api/payment']
|
|
87
|
+
}));
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Advanced (Per-Path Modes)
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
app.use(sentinel({
|
|
94
|
+
apiKey: process.env.SENTINEL_KEY,
|
|
95
|
+
|
|
96
|
+
protect: {
|
|
97
|
+
'/login': 'strict', // Zero tolerance
|
|
98
|
+
'/signup': 'strict', // Zero tolerance
|
|
99
|
+
'/api/search': 'balanced', // Block obvious abuse
|
|
100
|
+
'/api/public/*': 'monitor' // Log only, never block
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
profile: 'signup', // Optimized for account creation
|
|
104
|
+
|
|
105
|
+
fail: 'closed', // Block if Sentinel is unreachable (secure default)
|
|
106
|
+
|
|
107
|
+
onBlock: (req, res, decision) => {
|
|
108
|
+
res.status(403).json({
|
|
109
|
+
error: 'Access denied',
|
|
110
|
+
reason: decision.reason,
|
|
111
|
+
// Optionally show widget for redemption
|
|
112
|
+
widget_url: decision.remediation?.widget_required
|
|
113
|
+
? 'https://yoursite.com/verify'
|
|
114
|
+
: null
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}));
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Modes
|
|
123
|
+
|
|
124
|
+
| Mode | Behavior | Use Case |
|
|
125
|
+
|------|----------|----------|
|
|
126
|
+
| **monitor** | Never blocks, logs only | Testing, analytics |
|
|
127
|
+
| **balanced** | Blocks obvious abuse | General API protection |
|
|
128
|
+
| **strict** | Fail-closed, no mercy | Login, payments, crypto |
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Security Profiles
|
|
133
|
+
|
|
134
|
+
Optimize decision thresholds for your use case:
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
profile: 'api' // General API protection (default)
|
|
138
|
+
profile: 'signup' // Account creation (stricter on datacenter IPs)
|
|
139
|
+
profile: 'payments' // Financial transactions (maximum scrutiny)
|
|
140
|
+
profile: 'crypto' // Cryptocurrency operations (zero tolerance)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Fail Strategies
|
|
146
|
+
|
|
147
|
+
### Fail Closed (Default - Recommended)
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
fail: 'closed' // If Sentinel is unreachable, block requests
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Why this matters:** This is security infrastructure, not analytics. If the trust engine is down, attackers shouldn't get in.
|
|
154
|
+
|
|
155
|
+
### Fail Open (High Availability)
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
fail: 'open' // If Sentinel is unreachable, allow requests
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Use this if uptime is more critical than security (e.g., public read-only APIs).
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Path Matching
|
|
166
|
+
|
|
167
|
+
Supports wildcards for flexible protection:
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
protect: {
|
|
171
|
+
'/api/*': 'monitor', // All /api routes
|
|
172
|
+
'/admin/**': 'strict', // All admin routes (recursive)
|
|
173
|
+
'/auth/login': 'strict', // Exact match
|
|
174
|
+
'/public/search': 'balanced' // Specific endpoint
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Getting Your API Key
|
|
181
|
+
|
|
182
|
+
1. Visit [Sentinel Dashboard](https://sentinel-engine.onrender.com)
|
|
183
|
+
2. Sign in with GitHub (OAuth, no password needed)
|
|
184
|
+
3. Copy your API key from the dashboard
|
|
185
|
+
4. Add to `.env`:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
SENTINEL_KEY=your_api_key_here
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Pricing
|
|
194
|
+
|
|
195
|
+
### Free Tier
|
|
196
|
+
- ✅ 1,000 decisions/month
|
|
197
|
+
- ✅ All protection modes
|
|
198
|
+
- ✅ Fast-path decisions (<50ms)
|
|
199
|
+
- ✅ Community support
|
|
200
|
+
|
|
201
|
+
### Premium ($6/mo)
|
|
202
|
+
- ✅ 500,000 decisions/month
|
|
203
|
+
- ✅ Real-time analytics dashboard
|
|
204
|
+
- ✅ Conditional widget (only show for high-risk IPs)
|
|
205
|
+
- ✅ Priority support
|
|
206
|
+
|
|
207
|
+
[Upgrade on Dashboard →](https://sentinel-engine.onrender.com/dashboard)
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Advanced Usage
|
|
212
|
+
|
|
213
|
+
### Custom IP Extraction
|
|
214
|
+
|
|
215
|
+
```javascript
|
|
216
|
+
import { sentinel, SentinelClient } from 'api-turnstile';
|
|
217
|
+
|
|
218
|
+
// Use the client directly for custom logic
|
|
219
|
+
const client = new SentinelClient(process.env.SENTINEL_KEY);
|
|
220
|
+
|
|
221
|
+
app.post('/custom', async (req, res) => {
|
|
222
|
+
const ip = req.headers['cf-connecting-ip']; // Cloudflare
|
|
223
|
+
|
|
224
|
+
const decision = await client.check({
|
|
225
|
+
target: ip,
|
|
226
|
+
profile: 'payments'
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (!decision.allow) {
|
|
230
|
+
return res.status(403).json({ error: decision.reason });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Continue with handler
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Debug Mode
|
|
238
|
+
|
|
239
|
+
```javascript
|
|
240
|
+
app.use(sentinel({
|
|
241
|
+
apiKey: process.env.SENTINEL_KEY,
|
|
242
|
+
protect: ['/api/*'],
|
|
243
|
+
debug: true // Logs all decisions to console
|
|
244
|
+
}));
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## TypeScript Support
|
|
250
|
+
|
|
251
|
+
Fully typed out of the box:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { sentinel, SentinelConfig, SentinelDecision } from 'api-turnstile';
|
|
255
|
+
|
|
256
|
+
const config: SentinelConfig = {
|
|
257
|
+
apiKey: process.env.SENTINEL_KEY!,
|
|
258
|
+
protect: {
|
|
259
|
+
'/login': 'strict'
|
|
260
|
+
},
|
|
261
|
+
onBlock: (req, res, decision: SentinelDecision) => {
|
|
262
|
+
res.status(403).json({ blocked: true });
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
app.use(sentinel(config));
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Performance
|
|
272
|
+
|
|
273
|
+
- **Decision Latency:** <50ms (p99)
|
|
274
|
+
- **Overhead:** ~2-5ms per protected request
|
|
275
|
+
- **Caching:** Intelligent ASN-based caching reduces API calls
|
|
276
|
+
- **Fail-Safe:** Timeout after 2 seconds (configurable)
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## How This Beats Cloudflare
|
|
281
|
+
|
|
282
|
+
| Feature | Cloudflare Turnstile | api-turnstile |
|
|
283
|
+
|---------|---------------------|---------------|
|
|
284
|
+
| **Protects** | Browser pages | APIs |
|
|
285
|
+
| **Requires JS** | Yes | No |
|
|
286
|
+
| **CAPTCHA fallback** | Yes | Never |
|
|
287
|
+
| **API-native** | No | Yes |
|
|
288
|
+
| **Decision latency** | ~200ms | <50ms |
|
|
289
|
+
| **Per-endpoint modes** | No | Yes |
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Examples
|
|
294
|
+
|
|
295
|
+
### Protect Login Endpoint
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
app.use(sentinel({
|
|
299
|
+
apiKey: process.env.SENTINEL_KEY,
|
|
300
|
+
protect: { '/auth/login': 'strict' },
|
|
301
|
+
profile: 'signup'
|
|
302
|
+
}));
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Monitor Public API
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
app.use(sentinel({
|
|
309
|
+
apiKey: process.env.SENTINEL_KEY,
|
|
310
|
+
protect: { '/api/public/*': 'monitor' },
|
|
311
|
+
fail: 'open' // Never block public endpoints
|
|
312
|
+
}));
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Multi-Tier Protection
|
|
316
|
+
|
|
317
|
+
```javascript
|
|
318
|
+
app.use(sentinel({
|
|
319
|
+
apiKey: process.env.SENTINEL_KEY,
|
|
320
|
+
protect: {
|
|
321
|
+
'/auth/*': 'strict',
|
|
322
|
+
'/api/write/*': 'balanced',
|
|
323
|
+
'/api/read/*': 'monitor'
|
|
324
|
+
}
|
|
325
|
+
}));
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## FAQ
|
|
331
|
+
|
|
332
|
+
### Does this work with serverless?
|
|
333
|
+
|
|
334
|
+
Yes. Works with Vercel, Netlify, AWS Lambda, and any Node.js environment.
|
|
335
|
+
|
|
336
|
+
### What about GDPR?
|
|
337
|
+
|
|
338
|
+
Sentinel is **stateless** and stores no PII. Only temporary IP reputation data (ASN, geolocation) is processed.
|
|
339
|
+
|
|
340
|
+
### Can I self-host Sentinel?
|
|
341
|
+
|
|
342
|
+
The engine is open-source. [See repo →](https://github.com/risksignal/sentinel)
|
|
343
|
+
|
|
344
|
+
### What happens if Sentinel is down?
|
|
345
|
+
|
|
346
|
+
Depends on your `fail` strategy:
|
|
347
|
+
- `fail: 'closed'` → Blocks requests (secure default)
|
|
348
|
+
- `fail: 'open'` → Allows requests (high availability)
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Roadmap
|
|
353
|
+
|
|
354
|
+
- [x] Fastify middleware
|
|
355
|
+
- [ ] Next.js Edge Runtime support
|
|
356
|
+
- [ ] Hono adapter
|
|
357
|
+
- [ ] Bun native support
|
|
358
|
+
- [ ] Custom webhook notifications
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Support
|
|
363
|
+
|
|
364
|
+
- 📖 [Documentation](https://sentinel.risksignal.name.ng/docs)
|
|
365
|
+
- 💬 [GitHub Issues](https://github.com/risksignal/sentinel/issues)
|
|
366
|
+
- 🐦 [Twitter Updates](https://twitter.com/risksignal)
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## License
|
|
371
|
+
|
|
372
|
+
MIT
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
**Built by developers who are tired of CAPTCHAs ruining conversion rates.**
|
|
377
|
+
|
|
378
|
+
If this saved you from bot attacks, [⭐ star the repo](https://github.com/risksignal/sentinel).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CheckParams, SentinelDecision } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* HTTP client for communicating with Sentinel decision engine
|
|
4
|
+
*/
|
|
5
|
+
export declare class SentinelClient {
|
|
6
|
+
private readonly endpoint;
|
|
7
|
+
private readonly apiKey;
|
|
8
|
+
private readonly timeout;
|
|
9
|
+
private readonly debug;
|
|
10
|
+
constructor(apiKey: string, endpoint?: string, timeout?: number, debug?: boolean);
|
|
11
|
+
/**
|
|
12
|
+
* Make a decision request to Sentinel API
|
|
13
|
+
*/
|
|
14
|
+
check(params: CheckParams): Promise<SentinelDecision>;
|
|
15
|
+
/**
|
|
16
|
+
* Health check to verify Sentinel API is reachable
|
|
17
|
+
*/
|
|
18
|
+
healthCheck(): Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=sentinel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentinel.d.ts","sourceRoot":"","sources":["../../src/client/sentinel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAiB,MAAM,UAAU,CAAC;AAExE;;GAEG;AACH,qBAAa,cAAc;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAU;gBAG5B,MAAM,EAAE,MAAM,EACd,QAAQ,GAAE,MAA8C,EACxD,OAAO,GAAE,MAAa,EACtB,KAAK,GAAE,OAAe;IAQ1B;;OAEG;IACG,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAwE3D;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;CAWxC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SentinelClient = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
/**
|
|
6
|
+
* HTTP client for communicating with Sentinel decision engine
|
|
7
|
+
*/
|
|
8
|
+
class SentinelClient {
|
|
9
|
+
constructor(apiKey, endpoint = 'https://sentinel.risksignal.name.ng', timeout = 2000, debug = false) {
|
|
10
|
+
this.apiKey = apiKey;
|
|
11
|
+
this.endpoint = endpoint.replace(/\/$/, ''); // Remove trailing slash
|
|
12
|
+
this.timeout = timeout;
|
|
13
|
+
this.debug = debug;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Make a decision request to Sentinel API
|
|
17
|
+
*/
|
|
18
|
+
async check(params) {
|
|
19
|
+
const url = `${this.endpoint}/v1/check?mode=decision`;
|
|
20
|
+
if (this.debug) {
|
|
21
|
+
console.log('[Sentinel] Making decision request:', { url, target: params.target, profile: params.profile });
|
|
22
|
+
}
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(url, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
'x-api-key': this.apiKey,
|
|
31
|
+
...(params.trustToken ? { 'x-sentinel-trust': params.trustToken } : {}),
|
|
32
|
+
...(params.bwtNonce ? { 'x-bwt-nonce': params.bwtNonce } : {})
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
target: params.target,
|
|
36
|
+
profile: params.profile,
|
|
37
|
+
privacy_mode: params.privacy_mode || 'full'
|
|
38
|
+
}),
|
|
39
|
+
signal: controller.signal
|
|
40
|
+
});
|
|
41
|
+
clearTimeout(timeoutId);
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
44
|
+
throw new types_1.SentinelError(`Sentinel API returned ${response.status}: ${errorText}`, response.status);
|
|
45
|
+
}
|
|
46
|
+
const decision = await response.json();
|
|
47
|
+
if (this.debug) {
|
|
48
|
+
console.log('[Sentinel] Decision received:', {
|
|
49
|
+
allow: decision.allow,
|
|
50
|
+
reason: decision.reason,
|
|
51
|
+
latency: decision.latency_ms
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return decision;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
if (error.name === 'AbortError') {
|
|
59
|
+
throw new types_1.SentinelError(`Sentinel API timeout after ${this.timeout}ms`, undefined, error);
|
|
60
|
+
}
|
|
61
|
+
if (error instanceof types_1.SentinelError) {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
throw new types_1.SentinelError(`Failed to connect to Sentinel: ${error.message}`, undefined, error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Health check to verify Sentinel API is reachable
|
|
69
|
+
*/
|
|
70
|
+
async healthCheck() {
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch(`${this.endpoint}/health`, {
|
|
73
|
+
method: 'GET',
|
|
74
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
75
|
+
});
|
|
76
|
+
return response.ok;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.SentinelClient = SentinelClient;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-turnstile
|
|
3
|
+
*
|
|
4
|
+
* Cloudflare Turnstile protects browsers — not APIs.
|
|
5
|
+
* Sentinel is a Turnstile for APIs.
|
|
6
|
+
* Block bots, scripts, and automation without CAPTCHAs.
|
|
7
|
+
*/
|
|
8
|
+
export { sentinel } from './middleware/express';
|
|
9
|
+
export { sentinelFastify } from './middleware/fastify';
|
|
10
|
+
export { SentinelClient } from './client/sentinel';
|
|
11
|
+
export type { SentinelConfig, SentinelDecision, SecurityProfile, ProtectionMode, FailStrategy, PathProtection, CheckParams, Verdict } from './types';
|
|
12
|
+
export { SentinelError, ConfigurationError } from './types';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAGvD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGnD,YAAY,EACR,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,YAAY,EACZ,cAAc,EACd,WAAW,EACX,OAAO,EACV,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* api-turnstile
|
|
4
|
+
*
|
|
5
|
+
* Cloudflare Turnstile protects browsers — not APIs.
|
|
6
|
+
* Sentinel is a Turnstile for APIs.
|
|
7
|
+
* Block bots, scripts, and automation without CAPTCHAs.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.ConfigurationError = exports.SentinelError = exports.SentinelClient = exports.sentinelFastify = exports.sentinel = void 0;
|
|
11
|
+
// Export middleware
|
|
12
|
+
var express_1 = require("./middleware/express");
|
|
13
|
+
Object.defineProperty(exports, "sentinel", { enumerable: true, get: function () { return express_1.sentinel; } });
|
|
14
|
+
var fastify_1 = require("./middleware/fastify");
|
|
15
|
+
Object.defineProperty(exports, "sentinelFastify", { enumerable: true, get: function () { return fastify_1.sentinelFastify; } });
|
|
16
|
+
// Export client (for advanced usage)
|
|
17
|
+
var sentinel_1 = require("./client/sentinel");
|
|
18
|
+
Object.defineProperty(exports, "SentinelClient", { enumerable: true, get: function () { return sentinel_1.SentinelClient; } });
|
|
19
|
+
// Export errors
|
|
20
|
+
var types_1 = require("./types");
|
|
21
|
+
Object.defineProperty(exports, "SentinelError", { enumerable: true, get: function () { return types_1.SentinelError; } });
|
|
22
|
+
Object.defineProperty(exports, "ConfigurationError", { enumerable: true, get: function () { return types_1.ConfigurationError; } });
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { SentinelConfig } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Express middleware factory for Sentinel protection
|
|
5
|
+
*/
|
|
6
|
+
export declare function sentinel(config: SentinelConfig): (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
|
7
|
+
//# sourceMappingURL=express.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/middleware/express.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAqD,MAAM,UAAU,CAAC;AA+G7F;;GAEG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,cAAc,IAwB7B,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,wDAqFhE"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sentinel = sentinel;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
const sentinel_1 = require("../client/sentinel");
|
|
6
|
+
/**
|
|
7
|
+
* Path matching utilities
|
|
8
|
+
*/
|
|
9
|
+
class PathMatcher {
|
|
10
|
+
/**
|
|
11
|
+
* Check if a path matches a pattern (supports wildcards)
|
|
12
|
+
*/
|
|
13
|
+
static matches(path, pattern) {
|
|
14
|
+
// Exact match
|
|
15
|
+
if (path === pattern)
|
|
16
|
+
return true;
|
|
17
|
+
// Wildcard match (e.g., /api/*)
|
|
18
|
+
if (pattern.includes('*')) {
|
|
19
|
+
const regexPattern = pattern
|
|
20
|
+
.replace(/\*/g, '.*')
|
|
21
|
+
.replace(/\//g, '\\/');
|
|
22
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
23
|
+
return regex.test(path);
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get the protection mode for a given path
|
|
29
|
+
*/
|
|
30
|
+
static getMode(path, protect) {
|
|
31
|
+
// Array format - default to 'balanced'
|
|
32
|
+
if (Array.isArray(protect)) {
|
|
33
|
+
const isProtected = protect.some(pattern => this.matches(path, pattern));
|
|
34
|
+
return isProtected ? 'balanced' : null;
|
|
35
|
+
}
|
|
36
|
+
// Object format - check each pattern
|
|
37
|
+
for (const [pattern, mode] of Object.entries(protect)) {
|
|
38
|
+
if (this.matches(path, pattern)) {
|
|
39
|
+
return mode;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate Sentinel configuration
|
|
47
|
+
*/
|
|
48
|
+
function validateConfig(config) {
|
|
49
|
+
if (!config.apiKey || typeof config.apiKey !== 'string') {
|
|
50
|
+
throw new types_1.ConfigurationError('apiKey is required and must be a string');
|
|
51
|
+
}
|
|
52
|
+
if (!config.protect) {
|
|
53
|
+
throw new types_1.ConfigurationError('protect configuration is required');
|
|
54
|
+
}
|
|
55
|
+
if (!Array.isArray(config.protect) && typeof config.protect !== 'object') {
|
|
56
|
+
throw new types_1.ConfigurationError('protect must be an array or object');
|
|
57
|
+
}
|
|
58
|
+
if (config.fail && !['open', 'closed'].includes(config.fail)) {
|
|
59
|
+
throw new types_1.ConfigurationError('fail must be either "open" or "closed"');
|
|
60
|
+
}
|
|
61
|
+
if (config.profile && !['api', 'signup', 'payments', 'crypto'].includes(config.profile)) {
|
|
62
|
+
throw new types_1.ConfigurationError('profile must be one of: api, signup, payments, crypto');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Extract client IP from request
|
|
67
|
+
*/
|
|
68
|
+
function getClientIP(req) {
|
|
69
|
+
// 0. Manual Mocking for Lab Testing
|
|
70
|
+
const mockIp = req.headers['x-sentinel-mock-ip'];
|
|
71
|
+
if (mockIp && typeof mockIp === 'string') {
|
|
72
|
+
return mockIp;
|
|
73
|
+
}
|
|
74
|
+
// 1. Check x-forwarded-for header
|
|
75
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
76
|
+
if (forwarded) {
|
|
77
|
+
const ips = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
|
78
|
+
return ips.split(',')[0].trim();
|
|
79
|
+
}
|
|
80
|
+
// Check x-real-ip header
|
|
81
|
+
const realIp = req.headers['x-real-ip'];
|
|
82
|
+
if (realIp && typeof realIp === 'string') {
|
|
83
|
+
return realIp;
|
|
84
|
+
}
|
|
85
|
+
// Fall back to req.ip
|
|
86
|
+
if (req.ip) {
|
|
87
|
+
// Remove IPv6 prefix if present
|
|
88
|
+
let ip = req.ip;
|
|
89
|
+
if (ip.startsWith('::ffff:')) {
|
|
90
|
+
ip = ip.substring(7);
|
|
91
|
+
}
|
|
92
|
+
if (ip === '::1') {
|
|
93
|
+
ip = '127.0.0.1';
|
|
94
|
+
}
|
|
95
|
+
return ip;
|
|
96
|
+
}
|
|
97
|
+
return '127.0.0.1';
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Express middleware factory for Sentinel protection
|
|
101
|
+
*/
|
|
102
|
+
function sentinel(config) {
|
|
103
|
+
// Validate configuration at initialization
|
|
104
|
+
validateConfig(config);
|
|
105
|
+
// Initialize client
|
|
106
|
+
const client = new sentinel_1.SentinelClient(config.apiKey, config.endpoint, config.timeout, config.debug);
|
|
107
|
+
const failStrategy = config.fail || 'closed';
|
|
108
|
+
const defaultProfile = config.profile || 'api';
|
|
109
|
+
if (config.debug) {
|
|
110
|
+
console.log('[Sentinel] Middleware initialized', {
|
|
111
|
+
endpoint: config.endpoint || 'default',
|
|
112
|
+
fail: failStrategy,
|
|
113
|
+
profile: defaultProfile
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// Return the actual middleware function
|
|
117
|
+
return async (req, res, next) => {
|
|
118
|
+
const path = req.path;
|
|
119
|
+
const mode = PathMatcher.getMode(path, config.protect);
|
|
120
|
+
// Path is not protected - pass through
|
|
121
|
+
if (!mode) {
|
|
122
|
+
return next();
|
|
123
|
+
}
|
|
124
|
+
// Monitor mode - log but don't block
|
|
125
|
+
if (mode === 'monitor') {
|
|
126
|
+
if (config.debug) {
|
|
127
|
+
console.log(`[Sentinel] Monitor mode for ${path} - logging only`);
|
|
128
|
+
}
|
|
129
|
+
// Fire and forget the check for logging purposes
|
|
130
|
+
const ip = getClientIP(req);
|
|
131
|
+
client.check({ target: ip, profile: defaultProfile }).catch(() => {
|
|
132
|
+
// Silently fail in monitor mode
|
|
133
|
+
});
|
|
134
|
+
return next();
|
|
135
|
+
}
|
|
136
|
+
// Get client IP and tokens
|
|
137
|
+
const ip = getClientIP(req);
|
|
138
|
+
const trustToken = req.headers['x-sentinel-trust'];
|
|
139
|
+
const bwtNonce = req.headers['x-bwt-nonce'];
|
|
140
|
+
try {
|
|
141
|
+
// Make decision request
|
|
142
|
+
const decision = await client.check({
|
|
143
|
+
target: ip,
|
|
144
|
+
profile: defaultProfile,
|
|
145
|
+
trustToken,
|
|
146
|
+
bwtNonce
|
|
147
|
+
});
|
|
148
|
+
// PASS - allow request through
|
|
149
|
+
if (decision.allow) {
|
|
150
|
+
if (config.debug) {
|
|
151
|
+
console.log(`[Sentinel] PASS for ${ip} on ${path}`);
|
|
152
|
+
}
|
|
153
|
+
return next();
|
|
154
|
+
}
|
|
155
|
+
// BLOCK - reject request
|
|
156
|
+
if (config.debug) {
|
|
157
|
+
console.log(`[Sentinel] BLOCK for ${ip} on ${path}: ${decision.reason}`);
|
|
158
|
+
}
|
|
159
|
+
// Use custom block handler if provided
|
|
160
|
+
if (config.onBlock) {
|
|
161
|
+
return config.onBlock(req, res, decision);
|
|
162
|
+
}
|
|
163
|
+
// Default block response
|
|
164
|
+
return res.status(403).json({
|
|
165
|
+
error: 'Request blocked by Sentinel',
|
|
166
|
+
reason: decision.reason,
|
|
167
|
+
remediation: decision.remediation
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
// Handle Sentinel API errors based on fail strategy
|
|
172
|
+
if (config.debug) {
|
|
173
|
+
console.error('[Sentinel] Error during check:', error);
|
|
174
|
+
}
|
|
175
|
+
if (failStrategy === 'open') {
|
|
176
|
+
// Fail open - allow request through
|
|
177
|
+
if (config.debug) {
|
|
178
|
+
console.log('[Sentinel] Failing open - allowing request');
|
|
179
|
+
}
|
|
180
|
+
return next();
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// Fail closed - block request (secure default)
|
|
184
|
+
if (config.debug) {
|
|
185
|
+
console.log('[Sentinel] Failing closed - blocking request');
|
|
186
|
+
}
|
|
187
|
+
return res.status(503).json({
|
|
188
|
+
error: 'Security verification unavailable',
|
|
189
|
+
message: 'Please try again later'
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
+
import { SentinelConfig } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Sentinel Fastify Middleware
|
|
5
|
+
*
|
|
6
|
+
* Cloudflare Turnstile protects browsers — not APIs.
|
|
7
|
+
* Sentinel is a Turnstile for APIs.
|
|
8
|
+
*/
|
|
9
|
+
export declare const sentinelFastify: (config: SentinelConfig) => (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
10
|
+
//# sourceMappingURL=fastify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../../src/middleware/fastify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAA0C,MAAM,SAAS,CAAC;AAE/F,OAAO,EAAE,cAAc,EAAoB,MAAM,UAAU,CAAC;AAE5D;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,QAAQ,cAAc,MA6BpC,SAAS,cAAc,EAAE,OAAO,YAAY,kBAgD7D,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sentinelFastify = void 0;
|
|
4
|
+
const sentinel_1 = require("../client/sentinel");
|
|
5
|
+
/**
|
|
6
|
+
* Sentinel Fastify Middleware
|
|
7
|
+
*
|
|
8
|
+
* Cloudflare Turnstile protects browsers — not APIs.
|
|
9
|
+
* Sentinel is a Turnstile for APIs.
|
|
10
|
+
*/
|
|
11
|
+
const sentinelFastify = (config) => {
|
|
12
|
+
const client = new sentinel_1.SentinelClient(config.apiKey, config.endpoint, config.timeout, config.debug);
|
|
13
|
+
const isPathProtected = (path) => {
|
|
14
|
+
if (!config.protect)
|
|
15
|
+
return null;
|
|
16
|
+
if (Array.isArray(config.protect)) {
|
|
17
|
+
return config.protect.includes(path) ? 'balanced' : null;
|
|
18
|
+
}
|
|
19
|
+
// Exact match
|
|
20
|
+
if (config.protect[path])
|
|
21
|
+
return config.protect[path];
|
|
22
|
+
// Search for wildcards (e.g. /api/*)
|
|
23
|
+
for (const pattern in config.protect) {
|
|
24
|
+
if (pattern.endsWith('*')) {
|
|
25
|
+
const base = pattern.slice(0, -1);
|
|
26
|
+
if (path.startsWith(base))
|
|
27
|
+
return config.protect[pattern];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
return async (request, reply) => {
|
|
33
|
+
const mode = isPathProtected(request.url);
|
|
34
|
+
if (!mode || mode === 'monitor') {
|
|
35
|
+
if (config.debug && mode === 'monitor') {
|
|
36
|
+
console.log(`[Sentinel] Monitoring: ${request.url}`);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const ip = request.headers['x-forwarded-for'] || request.ip || '127.0.0.1';
|
|
41
|
+
const cleanIp = ip.includes(',') ? ip.split(',')[0].trim() : ip;
|
|
42
|
+
try {
|
|
43
|
+
const decision = await client.check({
|
|
44
|
+
target: cleanIp,
|
|
45
|
+
profile: config.profile || 'api',
|
|
46
|
+
privacy_mode: 'full',
|
|
47
|
+
trustToken: request.headers['x-sentinel-trust']
|
|
48
|
+
});
|
|
49
|
+
if (config.debug) {
|
|
50
|
+
console.log(`[Sentinel] Decision for ${cleanIp}: ${decision.allow ? 'ALLOW' : 'BLOCK'} (${decision.reason})`);
|
|
51
|
+
}
|
|
52
|
+
if (!decision.allow) {
|
|
53
|
+
if (config.onBlock) {
|
|
54
|
+
return config.onBlock(request, reply, decision);
|
|
55
|
+
}
|
|
56
|
+
return reply.code(403).send({
|
|
57
|
+
error: 'Access denied',
|
|
58
|
+
reason: decision.reason,
|
|
59
|
+
remediation: decision.remediation
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (config.debug) {
|
|
65
|
+
console.error('[Sentinel] Check failed:', error);
|
|
66
|
+
}
|
|
67
|
+
if (config.fail === 'closed') {
|
|
68
|
+
return reply.code(403).send({
|
|
69
|
+
error: 'Security system unavailable',
|
|
70
|
+
code: 'SENTINEL_UNREACHABLE'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
exports.sentinelFastify = sentinelFastify;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security profile types for different endpoint sensitivities
|
|
3
|
+
*/
|
|
4
|
+
export type SecurityProfile = 'api' | 'signup' | 'payments' | 'crypto';
|
|
5
|
+
/**
|
|
6
|
+
* Protection modes that determine blocking behavior
|
|
7
|
+
*/
|
|
8
|
+
export type ProtectionMode = 'monitor' | 'balanced' | 'strict';
|
|
9
|
+
/**
|
|
10
|
+
* Fail strategy when Sentinel API is unreachable
|
|
11
|
+
*/
|
|
12
|
+
export type FailStrategy = 'open' | 'closed';
|
|
13
|
+
/**
|
|
14
|
+
* Verdict from Sentinel decision engine
|
|
15
|
+
*/
|
|
16
|
+
export type Verdict = 'TRUSTED' | 'NEUTRAL' | 'UNTRUSTED';
|
|
17
|
+
/**
|
|
18
|
+
* Path protection configuration
|
|
19
|
+
*/
|
|
20
|
+
export type PathProtection = string[] | Record<string, ProtectionMode>;
|
|
21
|
+
/**
|
|
22
|
+
* Decision response from Sentinel API
|
|
23
|
+
*/
|
|
24
|
+
export interface SentinelDecision {
|
|
25
|
+
allow: boolean;
|
|
26
|
+
action: 'allow' | 'block';
|
|
27
|
+
http_status: number;
|
|
28
|
+
risk: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
confidence: number;
|
|
31
|
+
remediation?: {
|
|
32
|
+
widget_required: boolean;
|
|
33
|
+
trust_token_eligible: boolean;
|
|
34
|
+
};
|
|
35
|
+
latency_ms?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Main configuration for Sentinel middleware
|
|
39
|
+
*/
|
|
40
|
+
export interface SentinelConfig {
|
|
41
|
+
/**
|
|
42
|
+
* Your Sentinel API key (get from dashboard)
|
|
43
|
+
*/
|
|
44
|
+
apiKey: string;
|
|
45
|
+
/**
|
|
46
|
+
* Paths to protect - can be array of strings or object mapping paths to modes
|
|
47
|
+
* @example ['/login', '/signup']
|
|
48
|
+
* @example { '/login': 'strict', '/api/*': 'monitor' }
|
|
49
|
+
*/
|
|
50
|
+
protect: PathProtection;
|
|
51
|
+
/**
|
|
52
|
+
* Default security profile for protected endpoints
|
|
53
|
+
* @default 'api'
|
|
54
|
+
*/
|
|
55
|
+
profile?: SecurityProfile;
|
|
56
|
+
/**
|
|
57
|
+
* Sentinel API endpoint URL
|
|
58
|
+
* @default 'https://sentinel.risksignal.name.ng'
|
|
59
|
+
*/
|
|
60
|
+
endpoint?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Fail strategy when Sentinel is unreachable
|
|
63
|
+
* - 'closed': Block requests (secure default)
|
|
64
|
+
* - 'open': Allow requests through
|
|
65
|
+
* @default 'closed'
|
|
66
|
+
*/
|
|
67
|
+
fail?: FailStrategy;
|
|
68
|
+
/**
|
|
69
|
+
* Request timeout in milliseconds
|
|
70
|
+
* @default 2000
|
|
71
|
+
*/
|
|
72
|
+
timeout?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Custom block handler
|
|
75
|
+
*/
|
|
76
|
+
onBlock?: (req: any, res: any, decision: SentinelDecision) => void;
|
|
77
|
+
/**
|
|
78
|
+
* Enable debug logging
|
|
79
|
+
* @default false
|
|
80
|
+
*/
|
|
81
|
+
debug?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Internal check parameters sent to Sentinel API
|
|
85
|
+
*/
|
|
86
|
+
export interface CheckParams {
|
|
87
|
+
target: string;
|
|
88
|
+
profile: SecurityProfile;
|
|
89
|
+
privacy_mode?: 'strict' | 'full';
|
|
90
|
+
trustToken?: string;
|
|
91
|
+
bwtNonce?: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Error thrown when Sentinel API fails
|
|
95
|
+
*/
|
|
96
|
+
export declare class SentinelError extends Error {
|
|
97
|
+
statusCode?: number | undefined;
|
|
98
|
+
originalError?: Error | undefined;
|
|
99
|
+
constructor(message: string, statusCode?: number | undefined, originalError?: Error | undefined);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Error thrown when configuration is invalid
|
|
103
|
+
*/
|
|
104
|
+
export declare class ConfigurationError extends Error {
|
|
105
|
+
constructor(message: string);
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEvE;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE/D;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE7C;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;AAE1D;;GAEG;AACH,MAAM,MAAM,cAAc,GACpB,MAAM,EAAE,GACR,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAErC;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE;QACV,eAAe,EAAE,OAAO,CAAC;QACzB,oBAAoB,EAAE,OAAO,CAAC;KACjC,CAAC;IACF,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC3B;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;;OAIG;IACH,OAAO,EAAE,cAAc,CAAC;IAExB;;;OAGG;IACH,OAAO,CAAC,EAAE,eAAe,CAAC;IAE1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;IAEpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAEnE;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,eAAe,CAAC;IACzB,YAAY,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,KAAK;IAGzB,UAAU,CAAC,EAAE,MAAM;IACnB,aAAa,CAAC,EAAE,KAAK;gBAF5B,OAAO,EAAE,MAAM,EACR,UAAU,CAAC,EAAE,MAAM,YAAA,EACnB,aAAa,CAAC,EAAE,KAAK,YAAA;CAMnC;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAK9B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConfigurationError = exports.SentinelError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown when Sentinel API fails
|
|
6
|
+
*/
|
|
7
|
+
class SentinelError extends Error {
|
|
8
|
+
constructor(message, statusCode, originalError) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.statusCode = statusCode;
|
|
11
|
+
this.originalError = originalError;
|
|
12
|
+
this.name = 'SentinelError';
|
|
13
|
+
Object.setPrototypeOf(this, SentinelError.prototype);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.SentinelError = SentinelError;
|
|
17
|
+
/**
|
|
18
|
+
* Error thrown when configuration is invalid
|
|
19
|
+
*/
|
|
20
|
+
class ConfigurationError extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'ConfigurationError';
|
|
24
|
+
Object.setPrototypeOf(this, ConfigurationError.prototype);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.ConfigurationError = ConfigurationError;
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "api-turnstile",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cloudflare Turnstile protects browsers — not APIs. Sentinel is a Turnstile for APIs. Block bots, scripts, and automation without CAPTCHAs.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch",
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"api",
|
|
15
|
+
"security",
|
|
16
|
+
"bot-detection",
|
|
17
|
+
"turnstile",
|
|
18
|
+
"captcha",
|
|
19
|
+
"middleware",
|
|
20
|
+
"express",
|
|
21
|
+
"fastify",
|
|
22
|
+
"sentinel",
|
|
23
|
+
"fraud-prevention",
|
|
24
|
+
"rate-limiting"
|
|
25
|
+
],
|
|
26
|
+
"author": "Sentinel Security",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/risksignal/sentinel"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/risksignal/sentinel/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://sentinel.risksignal.name.ng",
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"express": "^4.0.0 || ^5.0.0",
|
|
38
|
+
"fastify": "^4.0.0 || ^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"express": {
|
|
42
|
+
"optional": true
|
|
43
|
+
},
|
|
44
|
+
"fastify": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/express": "^4.17.21",
|
|
50
|
+
"@types/node": "^20.11.0",
|
|
51
|
+
"fastify": "^5.7.1",
|
|
52
|
+
"typescript": "^5.3.3"
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"dist",
|
|
56
|
+
"README.md",
|
|
57
|
+
"LICENSE"
|
|
58
|
+
],
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=18.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|