api-turnstile 0.1.0 → 0.1.2

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 CHANGED
@@ -1,27 +1,7 @@
1
- # 🛡️ api-turnstile
1
+ # api-turnstile
2
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
- ---
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', '/signup']
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
- That's it. **92% of bot signups blocked automatically.**
56
-
57
- ---
58
-
59
- ## How It Works
23
+ ## Features
60
24
 
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
- ```
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
- No JavaScript required. No browser fingerprinting. **Pure API-native security.**
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
- ### 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
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
- Optimize decision thresholds for your use case:
50
+ ## Links
135
51
 
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
- ---
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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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;IAwE3D;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;CAWxC"}
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"}
@@ -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';
@@ -1 +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"}
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
- // Export client (for advanced usage)
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 middleware factory for Sentinel protection
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;AA+G7F;;GAEG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,cAAc,IAwB7B,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,wDAqFhE"}
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 and must be a string');
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 middleware factory for Sentinel protection
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
- // Path is not protected - pass through
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
- // PASS - allow request through
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: 'Request blocked by Sentinel',
100
+ error: 'Access denied',
166
101
  reason: decision.reason,
167
102
  remediation: decision.remediation
168
103
  });
169
104
  }
170
105
  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
- }
106
+ if (failStrategy === 'open')
180
107
  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
- }
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,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"}
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,6 @@
1
+ import { SentinelConfig } from '../types';
2
+ /**
3
+ * Sentinel Hono Middleware
4
+ */
5
+ export declare const sentinelHono: (config: SentinelConfig) => (c: any, next: any) => Promise<any>;
6
+ //# sourceMappingURL=hono.d.ts.map
@@ -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
@@ -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.0",
3
+ "version": "0.1.2",
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",
@@ -27,10 +30,10 @@
27
30
  "license": "MIT",
28
31
  "repository": {
29
32
  "type": "git",
30
- "url": "https://github.com/risksignal/sentinel"
33
+ "url": "https://github.com/00xf5/sentinelapinpm.git"
31
34
  },
32
35
  "bugs": {
33
- "url": "https://github.com/risksignal/sentinel/issues"
36
+ "url": "https://github.com/00xf5/sentinelapinpm/issues"
34
37
  },
35
38
  "homepage": "https://sentinel.risksignal.name.ng",
36
39
  "peerDependencies": {