api-turnstile 0.1.0 → 0.1.1
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/README.md +29 -349
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +176 -0
- package/dist/client/sentinel.d.ts.map +1 -1
- package/dist/client/sentinel.js +2 -1
- package/dist/index.d.ts +2 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -10
- package/dist/middleware/express.d.ts +1 -1
- package/dist/middleware/express.d.ts.map +1 -1
- package/dist/middleware/express.js +23 -103
- package/dist/middleware/fastify.d.ts +0 -3
- package/dist/middleware/fastify.d.ts.map +1 -1
- package/dist/middleware/fastify.js +1 -16
- package/dist/middleware/hono.d.ts +6 -0
- package/dist/middleware/hono.d.ts.map +1 -0
- package/dist/middleware/hono.js +67 -0
- package/dist/middleware/next.d.ts +16 -0
- package/dist/middleware/next.d.ts.map +1 -0
- package/dist/middleware/next.js +78 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,27 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# api-turnstile
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
---
|
|
3
|
+
Cloudflare Turnstile protects browsers. Sentinel protects APIs.
|
|
4
|
+
Block bots, scrapers, and automated attacks in under 50ms using infrastructure and behavioral signals. No CAPTCHAs required.
|
|
25
5
|
|
|
26
6
|
## Installation
|
|
27
7
|
|
|
@@ -29,350 +9,50 @@ Traditional solutions force you to choose between:
|
|
|
29
9
|
npm install api-turnstile
|
|
30
10
|
```
|
|
31
11
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
## Quick Start
|
|
35
|
-
|
|
36
|
-
Protect your signup endpoint in **under 60 seconds**:
|
|
12
|
+
## Quick Start (Express)
|
|
37
13
|
|
|
38
14
|
```javascript
|
|
39
|
-
import express from 'express';
|
|
40
15
|
import { sentinel } from 'api-turnstile';
|
|
41
16
|
|
|
42
|
-
const app = express();
|
|
43
|
-
|
|
44
17
|
app.use(sentinel({
|
|
45
18
|
apiKey: process.env.SENTINEL_KEY,
|
|
46
|
-
protect: ['/login', '/
|
|
19
|
+
protect: ['/login', '/api/*']
|
|
47
20
|
}));
|
|
48
|
-
|
|
49
|
-
app.post('/signup', (req, res) => {
|
|
50
|
-
// Only legitimate traffic reaches here
|
|
51
|
-
res.json({ success: true });
|
|
52
|
-
});
|
|
53
21
|
```
|
|
54
22
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
## How It Works
|
|
23
|
+
## Features
|
|
60
24
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
```
|
|
25
|
+
- **Multi-Framework**: Native support for Express, Fastify, Hono, and Bun.
|
|
26
|
+
- **Edge Native**: Specialized middleware for Next.js Edge Runtime and Vercel.
|
|
27
|
+
- **Sentinel CLI**: Terminal-based monitoring (`sentinel tail`) and forensics.
|
|
28
|
+
- **Economic Defenses**: Behavioral Work Tokens (BWT) to increase bot costs.
|
|
29
|
+
- **Real-time Alerts**: Webhook notifications for blocked incidents.
|
|
74
30
|
|
|
75
|
-
|
|
31
|
+
## Supported Adapters
|
|
76
32
|
|
|
77
|
-
|
|
33
|
+
| Framework | Middleware |
|
|
34
|
+
|-----------|------------|
|
|
35
|
+
| Express / Node | `sentinel(config)` |
|
|
36
|
+
| Fastify | `sentinelFastify(config)` |
|
|
37
|
+
| Hono / Bun | `sentinelHono(config)` |
|
|
38
|
+
| Next.js Edge | `sentinelEdge(config)` |
|
|
78
39
|
|
|
79
40
|
## Configuration
|
|
80
41
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
42
|
+
| Option | Description |
|
|
43
|
+
|--------|-------------|
|
|
44
|
+
| `apiKey` | Your API key from the dashboard |
|
|
45
|
+
| `protect` | Array of paths or path-to-mode mapping |
|
|
46
|
+
| `profile` | Security profile (`api`, `signup`, `payments`, `crypto`) |
|
|
47
|
+
| `webhooks` | Optional URL for block notifications |
|
|
48
|
+
| `fail` | Strategy if API is down (`open`, `closed`) |
|
|
133
49
|
|
|
134
|
-
|
|
50
|
+
## Links
|
|
135
51
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
---
|
|
52
|
+
- [Dashboard & API Keys](https://sentinel.risksignal.name.ng)
|
|
53
|
+
- [Full Documentation](https://sentinel.risksignal.name.ng/docs)
|
|
54
|
+
- [GitHub Repository](https://github.com/risksignal/sentinel)
|
|
369
55
|
|
|
370
56
|
## License
|
|
371
57
|
|
|
372
58
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const sentinel_1 = require("../client/sentinel");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf8'));
|
|
41
|
+
const usage = `
|
|
42
|
+
Sentinel CLI v${pkg.version}
|
|
43
|
+
The deterministic trust layer for modern APIs.
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
sentinel <command> [options]
|
|
47
|
+
|
|
48
|
+
Commands:
|
|
49
|
+
check <ip> Perform a real-time reputation and trust check on an IP
|
|
50
|
+
stats View aggregate security outcomes and mitigation rates
|
|
51
|
+
tail Stream live trust decisions to your terminal
|
|
52
|
+
scan <url> Simulate a security audit on an endpoint (CI/CD ready)
|
|
53
|
+
version Show current version
|
|
54
|
+
|
|
55
|
+
Options:
|
|
56
|
+
--key <key> Specify API key (defaults to SENTINEL_API_KEY env var)
|
|
57
|
+
--mock <ip> Mock IP for the scan (default: 1.1.1.1)
|
|
58
|
+
--profile <p> Decision profile (default: api)
|
|
59
|
+
--debug Enable verbose logging
|
|
60
|
+
`;
|
|
61
|
+
async function main() {
|
|
62
|
+
const args = process.argv.slice(2);
|
|
63
|
+
const command = args[0];
|
|
64
|
+
if (!command || command === 'help' || command === '--help') {
|
|
65
|
+
console.log(usage);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (command === 'version' || command === '--version' || command === '-v') {
|
|
69
|
+
console.log(`sentinel v${pkg.version}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let apiKey = process.env.SENTINEL_API_KEY;
|
|
73
|
+
const keyIndex = args.indexOf('--key');
|
|
74
|
+
if (keyIndex !== -1 && args[keyIndex + 1]) {
|
|
75
|
+
apiKey = args[keyIndex + 1];
|
|
76
|
+
}
|
|
77
|
+
if (!apiKey) {
|
|
78
|
+
console.error('Error: No API key found. Set SENTINEL_API_KEY or use --key.');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const debug = args.includes('--debug');
|
|
82
|
+
const client = new sentinel_1.SentinelClient(apiKey, 'https://sentinel.risksignal.name.ng', 5000, debug);
|
|
83
|
+
try {
|
|
84
|
+
switch (command) {
|
|
85
|
+
case 'scan':
|
|
86
|
+
const url = args[1];
|
|
87
|
+
if (!url) {
|
|
88
|
+
console.error('Error: Please specify a target URL.');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
const mockIp = args.includes('--mock') ? args[args.indexOf('--mock') + 1] : '1.1.1.1';
|
|
92
|
+
console.log(`Scanning security layer at ${url}`);
|
|
93
|
+
console.log(`Simulating origin: ${mockIp}\n`);
|
|
94
|
+
const start = Date.now();
|
|
95
|
+
const scanRes = await fetch(url, {
|
|
96
|
+
headers: {
|
|
97
|
+
'x-sentinel-mock-ip': mockIp,
|
|
98
|
+
'x-forwarded-for': mockIp
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const duration = Date.now() - start;
|
|
102
|
+
if (scanRes.status === 403) {
|
|
103
|
+
console.log(`PASS: Sentinel correctly blocked the request.`);
|
|
104
|
+
console.log(`Latency: ${duration}ms`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.log(`FAIL: Hostile traffic reached the origin (HTTP ${scanRes.status}).`);
|
|
108
|
+
console.log(`Check if your middleware is correctly applied to this path.`);
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
case 'check':
|
|
112
|
+
const ip = args[1];
|
|
113
|
+
if (!ip) {
|
|
114
|
+
console.error('Error: Please specify an IP address.');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const profile = args.includes('--profile') ? args[args.indexOf('--profile') + 1] : 'api';
|
|
118
|
+
console.log(`Checking trust for ${ip} [Profile: ${profile}]...`);
|
|
119
|
+
const decision = await client.check({ target: ip, profile: profile });
|
|
120
|
+
console.log(`Verdict: ${decision.allow ? 'PASS' : 'BLOCK'}`);
|
|
121
|
+
console.log(`Confidence: ${(decision.confidence * 100).toFixed(1)}%`);
|
|
122
|
+
console.log(`Reason: ${decision.reason.toUpperCase()}`);
|
|
123
|
+
console.log(`Latency: ${decision.latency_ms}ms`);
|
|
124
|
+
break;
|
|
125
|
+
case 'stats':
|
|
126
|
+
const statsRes = await fetch('https://sentinel.risksignal.name.ng/api/analytics', {
|
|
127
|
+
headers: { 'Authorization': `Bearer ${apiKey}` }
|
|
128
|
+
});
|
|
129
|
+
if (!statsRes.ok) {
|
|
130
|
+
const err = await statsRes.text();
|
|
131
|
+
console.error(`Error: Failed to fetch stats: ${err}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const stats = await statsRes.json();
|
|
135
|
+
console.log(`System Statistics`);
|
|
136
|
+
console.log(`-----------------`);
|
|
137
|
+
console.log(`Total Signals: ${stats.total_signals.toLocaleString()}`);
|
|
138
|
+
console.log(`Mitigation Rate: ${stats.outcomes.reduction}`);
|
|
139
|
+
console.log(`Blocked Attacks: ${stats.outcomes.blocked.toLocaleString()}`);
|
|
140
|
+
console.log(`Capital Saved: $${stats.outcomes.saved}`);
|
|
141
|
+
break;
|
|
142
|
+
case 'tail':
|
|
143
|
+
let lastSeen = new Date().toISOString();
|
|
144
|
+
console.log('Streaming live decisions (Press Ctrl+C to stop)...');
|
|
145
|
+
setInterval(async () => {
|
|
146
|
+
try {
|
|
147
|
+
const tailRes = await fetch('https://sentinel.risksignal.name.ng/api/analytics', {
|
|
148
|
+
headers: { 'Authorization': `Bearer ${apiKey}` }
|
|
149
|
+
});
|
|
150
|
+
if (!tailRes.ok)
|
|
151
|
+
return;
|
|
152
|
+
const data = await tailRes.json();
|
|
153
|
+
const logs = (data.recent_logs || []).reverse();
|
|
154
|
+
for (const log of logs) {
|
|
155
|
+
if (new Date(log.time) > new Date(lastSeen)) {
|
|
156
|
+
const v = log.verdict.toUpperCase();
|
|
157
|
+
const label = v === 'TRUSTED' ? 'PASS' : v === 'UNSTABLE' ? 'FLAG' : 'BLOCK';
|
|
158
|
+
console.log(`${new Date(log.time).toLocaleTimeString()} [${label}] ${log.target.padEnd(15)} | ${log.reason} (${log.latency}ms)`);
|
|
159
|
+
lastSeen = log.time;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (e) { }
|
|
164
|
+
}, 2000);
|
|
165
|
+
break;
|
|
166
|
+
default:
|
|
167
|
+
console.log(`Error: Unknown command: ${command}`);
|
|
168
|
+
console.log(usage);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
console.error(`Error: ${error.message}`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
main();
|
|
@@ -1 +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;
|
|
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;IAyE3D;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;CAWxC"}
|
package/dist/client/sentinel.js
CHANGED
|
@@ -34,7 +34,8 @@ class SentinelClient {
|
|
|
34
34
|
body: JSON.stringify({
|
|
35
35
|
target: params.target,
|
|
36
36
|
profile: params.profile,
|
|
37
|
-
privacy_mode: params.privacy_mode || 'full'
|
|
37
|
+
privacy_mode: params.privacy_mode || 'full',
|
|
38
|
+
bwt_enabled: !!params.bwtNonce
|
|
38
39
|
}),
|
|
39
40
|
signal: controller.signal
|
|
40
41
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
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
1
|
export { sentinel } from './middleware/express';
|
|
9
2
|
export { sentinelFastify } from './middleware/fastify';
|
|
3
|
+
export { sentinelEdge } from './middleware/next';
|
|
4
|
+
export { sentinelHono } from './middleware/hono';
|
|
10
5
|
export { SentinelClient } from './client/sentinel';
|
|
11
6
|
export type { SentinelConfig, SentinelDecision, SecurityProfile, ProtectionMode, FailStrategy, PathProtection, CheckParams, Verdict } from './types';
|
|
12
7
|
export { SentinelError, ConfigurationError } from './types';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,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;AAEjB,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.ConfigurationError = exports.SentinelError = exports.SentinelClient = exports.sentinelFastify = exports.sentinel = void 0;
|
|
3
|
+
exports.ConfigurationError = exports.SentinelError = exports.SentinelClient = exports.sentinelHono = exports.sentinelEdge = exports.sentinelFastify = exports.sentinel = void 0;
|
|
11
4
|
// Export middleware
|
|
12
5
|
var express_1 = require("./middleware/express");
|
|
13
6
|
Object.defineProperty(exports, "sentinel", { enumerable: true, get: function () { return express_1.sentinel; } });
|
|
14
7
|
var fastify_1 = require("./middleware/fastify");
|
|
15
8
|
Object.defineProperty(exports, "sentinelFastify", { enumerable: true, get: function () { return fastify_1.sentinelFastify; } });
|
|
16
|
-
|
|
9
|
+
var next_1 = require("./middleware/next");
|
|
10
|
+
Object.defineProperty(exports, "sentinelEdge", { enumerable: true, get: function () { return next_1.sentinelEdge; } });
|
|
11
|
+
var hono_1 = require("./middleware/hono");
|
|
12
|
+
Object.defineProperty(exports, "sentinelHono", { enumerable: true, get: function () { return hono_1.sentinelHono; } });
|
|
13
|
+
// Export client
|
|
17
14
|
var sentinel_1 = require("./client/sentinel");
|
|
18
15
|
Object.defineProperty(exports, "SentinelClient", { enumerable: true, get: function () { return sentinel_1.SentinelClient; } });
|
|
19
|
-
// Export errors
|
|
20
16
|
var types_1 = require("./types");
|
|
21
17
|
Object.defineProperty(exports, "SentinelError", { enumerable: true, get: function () { return types_1.SentinelError; } });
|
|
22
18
|
Object.defineProperty(exports, "ConfigurationError", { enumerable: true, get: function () { return types_1.ConfigurationError; } });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
2
|
import { SentinelConfig } from '../types';
|
|
3
3
|
/**
|
|
4
|
-
* Express
|
|
4
|
+
* Sentinel Express Middleware
|
|
5
5
|
*/
|
|
6
6
|
export declare function sentinel(config: SentinelConfig): (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
|
7
7
|
//# sourceMappingURL=express.d.ts.map
|
|
@@ -1 +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;
|
|
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;AA0D7F;;GAEG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,cAAc,IAa7B,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,wDAqDhE"}
|
|
@@ -3,192 +3,112 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.sentinel = sentinel;
|
|
4
4
|
const types_1 = require("../types");
|
|
5
5
|
const sentinel_1 = require("../client/sentinel");
|
|
6
|
-
/**
|
|
7
|
-
* Path matching utilities
|
|
8
|
-
*/
|
|
9
6
|
class PathMatcher {
|
|
10
|
-
/**
|
|
11
|
-
* Check if a path matches a pattern (supports wildcards)
|
|
12
|
-
*/
|
|
13
7
|
static matches(path, pattern) {
|
|
14
|
-
// Exact match
|
|
15
8
|
if (path === pattern)
|
|
16
9
|
return true;
|
|
17
|
-
// Wildcard match (e.g., /api/*)
|
|
18
10
|
if (pattern.includes('*')) {
|
|
19
|
-
const regexPattern = pattern
|
|
20
|
-
.replace(/\*/g, '.*')
|
|
21
|
-
.replace(/\//g, '\\/');
|
|
11
|
+
const regexPattern = pattern.replace(/\*/g, '.*').replace(/\//g, '\\/');
|
|
22
12
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
23
13
|
return regex.test(path);
|
|
24
14
|
}
|
|
25
15
|
return false;
|
|
26
16
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Get the protection mode for a given path
|
|
29
|
-
*/
|
|
30
17
|
static getMode(path, protect) {
|
|
31
|
-
// Array format - default to 'balanced'
|
|
32
18
|
if (Array.isArray(protect)) {
|
|
33
19
|
const isProtected = protect.some(pattern => this.matches(path, pattern));
|
|
34
20
|
return isProtected ? 'balanced' : null;
|
|
35
21
|
}
|
|
36
|
-
// Object format - check each pattern
|
|
37
22
|
for (const [pattern, mode] of Object.entries(protect)) {
|
|
38
|
-
if (this.matches(path, pattern))
|
|
23
|
+
if (this.matches(path, pattern))
|
|
39
24
|
return mode;
|
|
40
|
-
}
|
|
41
25
|
}
|
|
42
26
|
return null;
|
|
43
27
|
}
|
|
44
28
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Validate Sentinel configuration
|
|
47
|
-
*/
|
|
48
29
|
function validateConfig(config) {
|
|
49
30
|
if (!config.apiKey || typeof config.apiKey !== 'string') {
|
|
50
|
-
throw new types_1.ConfigurationError('apiKey is required
|
|
31
|
+
throw new types_1.ConfigurationError('apiKey is required');
|
|
51
32
|
}
|
|
52
33
|
if (!config.protect) {
|
|
53
34
|
throw new types_1.ConfigurationError('protect configuration is required');
|
|
54
35
|
}
|
|
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
36
|
}
|
|
65
|
-
/**
|
|
66
|
-
* Extract client IP from request
|
|
67
|
-
*/
|
|
68
37
|
function getClientIP(req) {
|
|
69
|
-
// 0. Manual Mocking for Lab Testing
|
|
70
38
|
const mockIp = req.headers['x-sentinel-mock-ip'];
|
|
71
|
-
if (mockIp && typeof mockIp === 'string')
|
|
39
|
+
if (mockIp && typeof mockIp === 'string')
|
|
72
40
|
return mockIp;
|
|
73
|
-
}
|
|
74
|
-
// 1. Check x-forwarded-for header
|
|
75
41
|
const forwarded = req.headers['x-forwarded-for'];
|
|
76
42
|
if (forwarded) {
|
|
77
43
|
const ips = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
|
78
44
|
return ips.split(',')[0].trim();
|
|
79
45
|
}
|
|
80
|
-
// Check x-real-ip header
|
|
81
46
|
const realIp = req.headers['x-real-ip'];
|
|
82
|
-
if (realIp && typeof realIp === 'string')
|
|
47
|
+
if (realIp && typeof realIp === 'string')
|
|
83
48
|
return realIp;
|
|
84
|
-
}
|
|
85
|
-
// Fall back to req.ip
|
|
86
49
|
if (req.ip) {
|
|
87
|
-
// Remove IPv6 prefix if present
|
|
88
50
|
let ip = req.ip;
|
|
89
|
-
if (ip.startsWith('::ffff:'))
|
|
51
|
+
if (ip.startsWith('::ffff:'))
|
|
90
52
|
ip = ip.substring(7);
|
|
91
|
-
|
|
92
|
-
if (ip === '::1') {
|
|
53
|
+
if (ip === '::1')
|
|
93
54
|
ip = '127.0.0.1';
|
|
94
|
-
}
|
|
95
55
|
return ip;
|
|
96
56
|
}
|
|
97
57
|
return '127.0.0.1';
|
|
98
58
|
}
|
|
99
59
|
/**
|
|
100
|
-
* Express
|
|
60
|
+
* Sentinel Express Middleware
|
|
101
61
|
*/
|
|
102
62
|
function sentinel(config) {
|
|
103
|
-
// Validate configuration at initialization
|
|
104
63
|
validateConfig(config);
|
|
105
|
-
// Initialize client
|
|
106
64
|
const client = new sentinel_1.SentinelClient(config.apiKey, config.endpoint, config.timeout, config.debug);
|
|
107
65
|
const failStrategy = config.fail || 'closed';
|
|
108
66
|
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
67
|
return async (req, res, next) => {
|
|
118
68
|
const path = req.path;
|
|
119
69
|
const mode = PathMatcher.getMode(path, config.protect);
|
|
120
|
-
|
|
121
|
-
if (!mode) {
|
|
70
|
+
if (!mode)
|
|
122
71
|
return next();
|
|
123
|
-
}
|
|
124
|
-
// Monitor mode - log but don't block
|
|
125
72
|
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
73
|
const ip = getClientIP(req);
|
|
131
|
-
client.check({ target: ip, profile: defaultProfile }).catch(() => {
|
|
132
|
-
// Silently fail in monitor mode
|
|
133
|
-
});
|
|
74
|
+
client.check({ target: ip, profile: defaultProfile }).catch(() => { });
|
|
134
75
|
return next();
|
|
135
76
|
}
|
|
136
|
-
// Get client IP and tokens
|
|
137
77
|
const ip = getClientIP(req);
|
|
138
78
|
const trustToken = req.headers['x-sentinel-trust'];
|
|
139
79
|
const bwtNonce = req.headers['x-bwt-nonce'];
|
|
140
80
|
try {
|
|
141
|
-
// Make decision request
|
|
142
81
|
const decision = await client.check({
|
|
143
82
|
target: ip,
|
|
144
83
|
profile: defaultProfile,
|
|
145
84
|
trustToken,
|
|
146
85
|
bwtNonce
|
|
147
86
|
});
|
|
148
|
-
|
|
149
|
-
if (decision.allow) {
|
|
150
|
-
if (config.debug) {
|
|
151
|
-
console.log(`[Sentinel] PASS for ${ip} on ${path}`);
|
|
152
|
-
}
|
|
87
|
+
if (decision.allow)
|
|
153
88
|
return next();
|
|
89
|
+
if (config.webhooks?.onBlock) {
|
|
90
|
+
fetch(config.webhooks.onBlock, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
body: JSON.stringify({ event: 'SENTINEL_BLOCK', ip, path, decision })
|
|
94
|
+
}).catch(() => { });
|
|
154
95
|
}
|
|
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
96
|
if (config.onBlock) {
|
|
161
97
|
return config.onBlock(req, res, decision);
|
|
162
98
|
}
|
|
163
|
-
// Default block response
|
|
164
99
|
return res.status(403).json({
|
|
165
|
-
error: '
|
|
100
|
+
error: 'Access denied',
|
|
166
101
|
reason: decision.reason,
|
|
167
102
|
remediation: decision.remediation
|
|
168
103
|
});
|
|
169
104
|
}
|
|
170
105
|
catch (error) {
|
|
171
|
-
|
|
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
|
-
}
|
|
106
|
+
if (failStrategy === 'open')
|
|
180
107
|
return next();
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
}
|
|
108
|
+
return res.status(503).json({
|
|
109
|
+
error: 'Security verification unavailable',
|
|
110
|
+
message: 'Please try again later'
|
|
111
|
+
});
|
|
192
112
|
}
|
|
193
113
|
};
|
|
194
114
|
}
|
|
@@ -2,9 +2,6 @@ import { FastifyRequest, FastifyReply } from 'fastify';
|
|
|
2
2
|
import { SentinelConfig } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* Sentinel Fastify Middleware
|
|
5
|
-
*
|
|
6
|
-
* Cloudflare Turnstile protects browsers — not APIs.
|
|
7
|
-
* Sentinel is a Turnstile for APIs.
|
|
8
5
|
*/
|
|
9
6
|
export declare const sentinelFastify: (config: SentinelConfig) => (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
10
7
|
//# sourceMappingURL=fastify.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../../src/middleware/fastify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../../src/middleware/fastify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1C;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,QAAQ,cAAc,MA2BpC,SAAS,cAAc,EAAE,OAAO,YAAY,kBAmC7D,CAAC"}
|
|
@@ -4,9 +4,6 @@ exports.sentinelFastify = void 0;
|
|
|
4
4
|
const sentinel_1 = require("../client/sentinel");
|
|
5
5
|
/**
|
|
6
6
|
* Sentinel Fastify Middleware
|
|
7
|
-
*
|
|
8
|
-
* Cloudflare Turnstile protects browsers — not APIs.
|
|
9
|
-
* Sentinel is a Turnstile for APIs.
|
|
10
7
|
*/
|
|
11
8
|
const sentinelFastify = (config) => {
|
|
12
9
|
const client = new sentinel_1.SentinelClient(config.apiKey, config.endpoint, config.timeout, config.debug);
|
|
@@ -16,10 +13,8 @@ const sentinelFastify = (config) => {
|
|
|
16
13
|
if (Array.isArray(config.protect)) {
|
|
17
14
|
return config.protect.includes(path) ? 'balanced' : null;
|
|
18
15
|
}
|
|
19
|
-
// Exact match
|
|
20
16
|
if (config.protect[path])
|
|
21
17
|
return config.protect[path];
|
|
22
|
-
// Search for wildcards (e.g. /api/*)
|
|
23
18
|
for (const pattern in config.protect) {
|
|
24
19
|
if (pattern.endsWith('*')) {
|
|
25
20
|
const base = pattern.slice(0, -1);
|
|
@@ -31,12 +26,8 @@ const sentinelFastify = (config) => {
|
|
|
31
26
|
};
|
|
32
27
|
return async (request, reply) => {
|
|
33
28
|
const mode = isPathProtected(request.url);
|
|
34
|
-
if (!mode || mode === 'monitor')
|
|
35
|
-
if (config.debug && mode === 'monitor') {
|
|
36
|
-
console.log(`[Sentinel] Monitoring: ${request.url}`);
|
|
37
|
-
}
|
|
29
|
+
if (!mode || mode === 'monitor')
|
|
38
30
|
return;
|
|
39
|
-
}
|
|
40
31
|
const ip = request.headers['x-forwarded-for'] || request.ip || '127.0.0.1';
|
|
41
32
|
const cleanIp = ip.includes(',') ? ip.split(',')[0].trim() : ip;
|
|
42
33
|
try {
|
|
@@ -46,9 +37,6 @@ const sentinelFastify = (config) => {
|
|
|
46
37
|
privacy_mode: 'full',
|
|
47
38
|
trustToken: request.headers['x-sentinel-trust']
|
|
48
39
|
});
|
|
49
|
-
if (config.debug) {
|
|
50
|
-
console.log(`[Sentinel] Decision for ${cleanIp}: ${decision.allow ? 'ALLOW' : 'BLOCK'} (${decision.reason})`);
|
|
51
|
-
}
|
|
52
40
|
if (!decision.allow) {
|
|
53
41
|
if (config.onBlock) {
|
|
54
42
|
return config.onBlock(request, reply, decision);
|
|
@@ -61,9 +49,6 @@ const sentinelFastify = (config) => {
|
|
|
61
49
|
}
|
|
62
50
|
}
|
|
63
51
|
catch (error) {
|
|
64
|
-
if (config.debug) {
|
|
65
|
-
console.error('[Sentinel] Check failed:', error);
|
|
66
|
-
}
|
|
67
52
|
if (config.fail === 'closed') {
|
|
68
53
|
return reply.code(403).send({
|
|
69
54
|
error: 'Security system unavailable',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../../src/middleware/hono.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1C;;GAEG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,cAAc,MA2BjC,GAAG,GAAG,EAAE,MAAM,GAAG,iBA6ClC,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sentinelHono = void 0;
|
|
4
|
+
const sentinel_1 = require("../client/sentinel");
|
|
5
|
+
/**
|
|
6
|
+
* Sentinel Hono Middleware
|
|
7
|
+
*/
|
|
8
|
+
const sentinelHono = (config) => {
|
|
9
|
+
const client = new sentinel_1.SentinelClient(config.apiKey, config.endpoint, config.timeout, config.debug);
|
|
10
|
+
const isPathProtected = (path) => {
|
|
11
|
+
if (!config.protect)
|
|
12
|
+
return null;
|
|
13
|
+
if (Array.isArray(config.protect)) {
|
|
14
|
+
return config.protect.includes(path) ? 'balanced' : null;
|
|
15
|
+
}
|
|
16
|
+
if (config.protect[path])
|
|
17
|
+
return config.protect[path];
|
|
18
|
+
for (const pattern in config.protect) {
|
|
19
|
+
if (pattern.endsWith('*')) {
|
|
20
|
+
const base = pattern.slice(0, -1);
|
|
21
|
+
if (path.startsWith(base))
|
|
22
|
+
return config.protect[pattern];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
return async (c, next) => {
|
|
28
|
+
const path = c.req.path;
|
|
29
|
+
const mode = isPathProtected(path);
|
|
30
|
+
if (!mode || mode === 'monitor') {
|
|
31
|
+
await next();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const ip = c.req.header('x-forwarded-for') ||
|
|
35
|
+
c.req.header('x-real-ip') ||
|
|
36
|
+
'127.0.0.1';
|
|
37
|
+
const cleanIp = ip.includes(',') ? ip.split(',')[0].trim() : ip;
|
|
38
|
+
try {
|
|
39
|
+
const decision = await client.check({
|
|
40
|
+
target: cleanIp,
|
|
41
|
+
profile: config.profile || 'api',
|
|
42
|
+
privacy_mode: 'full',
|
|
43
|
+
trustToken: c.req.header('x-sentinel-trust')
|
|
44
|
+
});
|
|
45
|
+
if (!decision.allow) {
|
|
46
|
+
if (config.onBlock) {
|
|
47
|
+
return config.onBlock(c.req, c.res, decision);
|
|
48
|
+
}
|
|
49
|
+
return c.json({
|
|
50
|
+
error: 'Access denied',
|
|
51
|
+
reason: decision.reason,
|
|
52
|
+
remediation: decision.remediation
|
|
53
|
+
}, 403);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (config.fail === 'closed') {
|
|
58
|
+
return c.json({
|
|
59
|
+
error: 'Security system unavailable',
|
|
60
|
+
code: 'SENTINEL_UNREACHABLE'
|
|
61
|
+
}, 403);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await next();
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
exports.sentinelHono = sentinelHono;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SentinelConfig } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Sentinel Next.js Edge Middleware
|
|
4
|
+
*/
|
|
5
|
+
export declare const sentinelEdge: (config: SentinelConfig) => (request: any) => Promise<{
|
|
6
|
+
blocked: boolean;
|
|
7
|
+
status: number;
|
|
8
|
+
decision: import("../types").SentinelDecision;
|
|
9
|
+
response: import("undici-types").Response;
|
|
10
|
+
} | {
|
|
11
|
+
blocked: boolean;
|
|
12
|
+
status: number;
|
|
13
|
+
response: import("undici-types").Response;
|
|
14
|
+
decision?: undefined;
|
|
15
|
+
} | null>;
|
|
16
|
+
//# sourceMappingURL=next.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../../src/middleware/next.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1C;;GAEG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,cAAc,MA2BjC,SAAS,GAAG;;;;;;;;;;SAsD7B,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sentinelEdge = void 0;
|
|
4
|
+
const sentinel_1 = require("../client/sentinel");
|
|
5
|
+
/**
|
|
6
|
+
* Sentinel Next.js Edge Middleware
|
|
7
|
+
*/
|
|
8
|
+
const sentinelEdge = (config) => {
|
|
9
|
+
const client = new sentinel_1.SentinelClient(config.apiKey, config.endpoint, config.timeout, config.debug);
|
|
10
|
+
const isPathProtected = (path) => {
|
|
11
|
+
if (!config.protect)
|
|
12
|
+
return null;
|
|
13
|
+
if (Array.isArray(config.protect)) {
|
|
14
|
+
return config.protect.includes(path) ? 'balanced' : null;
|
|
15
|
+
}
|
|
16
|
+
if (config.protect[path])
|
|
17
|
+
return config.protect[path];
|
|
18
|
+
for (const pattern in config.protect) {
|
|
19
|
+
if (pattern.endsWith('*')) {
|
|
20
|
+
const base = pattern.slice(0, -1);
|
|
21
|
+
if (path.startsWith(base))
|
|
22
|
+
return config.protect[pattern];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
return async (request) => {
|
|
28
|
+
const url = new URL(request.url);
|
|
29
|
+
const path = url.pathname;
|
|
30
|
+
const mode = isPathProtected(path);
|
|
31
|
+
if (!mode || mode === 'monitor')
|
|
32
|
+
return null;
|
|
33
|
+
const ip = request.headers.get('x-forwarded-for') ||
|
|
34
|
+
request.headers.get('x-real-ip') ||
|
|
35
|
+
'127.0.0.1';
|
|
36
|
+
const cleanIp = ip.includes(',') ? ip.split(',')[0].trim() : ip;
|
|
37
|
+
try {
|
|
38
|
+
const decision = await client.check({
|
|
39
|
+
target: cleanIp,
|
|
40
|
+
profile: config.profile || 'api',
|
|
41
|
+
privacy_mode: 'full',
|
|
42
|
+
trustToken: request.headers.get('x-sentinel-trust')
|
|
43
|
+
});
|
|
44
|
+
if (!decision.allow) {
|
|
45
|
+
return {
|
|
46
|
+
blocked: true,
|
|
47
|
+
status: 403,
|
|
48
|
+
decision,
|
|
49
|
+
response: new Response(JSON.stringify({
|
|
50
|
+
error: 'Access denied',
|
|
51
|
+
reason: decision.reason,
|
|
52
|
+
remediation: decision.remediation
|
|
53
|
+
}), {
|
|
54
|
+
status: 403,
|
|
55
|
+
headers: { 'Content-Type': 'application/json' }
|
|
56
|
+
})
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (config.fail === 'closed') {
|
|
62
|
+
return {
|
|
63
|
+
blocked: true,
|
|
64
|
+
status: 403,
|
|
65
|
+
response: new Response(JSON.stringify({
|
|
66
|
+
error: 'Security system unavailable',
|
|
67
|
+
code: 'SENTINEL_UNREACHABLE'
|
|
68
|
+
}), {
|
|
69
|
+
status: 403,
|
|
70
|
+
headers: { 'Content-Type': 'application/json' }
|
|
71
|
+
})
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
exports.sentinelEdge = sentinelEdge;
|
package/dist/types.d.ts
CHANGED
|
@@ -74,6 +74,21 @@ export interface SentinelConfig {
|
|
|
74
74
|
* Custom block handler
|
|
75
75
|
*/
|
|
76
76
|
onBlock?: (req: any, res: any, decision: SentinelDecision) => void;
|
|
77
|
+
/**
|
|
78
|
+
* Webhook notifications for security events
|
|
79
|
+
*/
|
|
80
|
+
webhooks?: {
|
|
81
|
+
onBlock?: string;
|
|
82
|
+
onFlag?: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Behavioral Work Token (BWT) options
|
|
86
|
+
* Enforces cryptographic proof-of-work for "Unstable" IPs
|
|
87
|
+
*/
|
|
88
|
+
bwt?: {
|
|
89
|
+
enabled: boolean;
|
|
90
|
+
difficulty?: number;
|
|
91
|
+
};
|
|
77
92
|
/**
|
|
78
93
|
* Enable debug logging
|
|
79
94
|
* @default false
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +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"}
|
|
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;;OAEG;IACH,QAAQ,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IAEF;;;OAGG;IACH,GAAG,CAAC,EAAE;QACF,OAAO,EAAE,OAAO,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IAEF;;;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/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api-turnstile",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Cloudflare Turnstile protects browsers — not APIs. Sentinel is a Turnstile for APIs. Block bots, scripts, and automation without CAPTCHAs.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sentinel": "dist/cli/index.js"
|
|
9
|
+
},
|
|
7
10
|
"scripts": {
|
|
8
11
|
"build": "tsc",
|
|
9
12
|
"dev": "tsc --watch",
|