sentinelle-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # sentinelle-agent
2
+
3
+ Sentinelle monitoring agent for **Node.js** apps. Supports **Express**, **Fastify**, **Koa**, **NestJS**.
4
+
5
+ Pair this with the official dashboard at [sentinelle.dev](https://sentinelle.dev) to get heartbeats, route discovery, error capture and request metrics — all visible in real time.
6
+
7
+ A Python sibling is published as [`sentinelle-agent` on PyPI](https://pypi.org/project/sentinelle-agent/) with identical wire format.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install sentinelle-agent
13
+ ```
14
+
15
+ ## Configuration
16
+
17
+ Two ways, equivalent:
18
+
19
+ **Env vars** (recommended in production):
20
+
21
+ ```bash
22
+ SENTINELLE_API_KEY=your-project-api-key
23
+ SENTINELLE_SERVER_URL=https://api.sentinelle.dev
24
+ SENTINELLE_APP_NAME=my-node-app
25
+ ```
26
+
27
+ **Code:**
28
+
29
+ ```js
30
+ const sentinelle = require('sentinelle-agent');
31
+
32
+ sentinelle.init({
33
+ apiKey: 'your-project-api-key',
34
+ serverUrl: 'https://api.sentinelle.dev',
35
+ appName: 'my-node-app',
36
+ });
37
+ ```
38
+
39
+ Get your `apiKey` from your Sentinelle dashboard — project settings.
40
+
41
+ ## Express
42
+
43
+ ```js
44
+ const express = require('express');
45
+ const sentinelle = require('sentinelle-agent');
46
+
47
+ sentinelle.init({ appName: 'my-app' });
48
+
49
+ const app = express();
50
+
51
+ // BEFORE your routes
52
+ app.use(sentinelle.middleware());
53
+
54
+ // your routes here
55
+ app.get('/hello', (req, res) => res.json({ ok: true }));
56
+
57
+ // AFTER your routes
58
+ app.use(sentinelle.errorHandler());
59
+
60
+ // Auto-discover all routes (call after registration)
61
+ sentinelle.discoverRoutes(app);
62
+
63
+ app.listen(3000);
64
+ ```
65
+
66
+ ## NestJS
67
+
68
+ ```ts
69
+ // main.ts
70
+ import * as sentinelle from 'sentinelle-agent';
71
+ import { NestFactory } from '@nestjs/core';
72
+ import { AppModule } from './app.module';
73
+
74
+ async function bootstrap() {
75
+ sentinelle.init({ appName: 'my-nestjs-app' });
76
+
77
+ const app = await NestFactory.create(AppModule);
78
+ const expressApp = app.getHttpAdapter().getInstance();
79
+ expressApp.use(sentinelle.middleware());
80
+ expressApp.use(sentinelle.errorHandler());
81
+
82
+ await app.listen(3000);
83
+ sentinelle.discoverRoutes(expressApp);
84
+ }
85
+ bootstrap();
86
+ ```
87
+
88
+ ## What it does
89
+
90
+ **Per-request** (Express middleware):
91
+ - Times each request, captures method/path/status/responseMs/IP/UA
92
+ - Aggregates top routes, errors 4xx/5xx, status distribution
93
+
94
+ **Errors:**
95
+ - Unhandled exceptions in routes (via `errorHandler` middleware)
96
+ - Uncaught exceptions and unhandled rejections at process level
97
+ - Slow requests (> 5000 ms) reported as warnings
98
+ - Sensitive fields auto-redacted in bodies: `password`, `token`, `secret`, `authorization`, `cookie`, `credit_card`, `cvv`, `ssn`
99
+
100
+ **Routes:**
101
+ - Discovered automatically via Express internal router stack
102
+ - Posted once to `/api/webhooks/agent/routes`
103
+
104
+ **Heartbeat** (every 60s):
105
+ - App uptime, RSS / heap memory, Node version
106
+ - Request metrics (total, avg ms, errors 4xx/5xx, top 5 routes)
107
+ - Posted to `/api/webhooks/agent/heartbeat`
108
+
109
+ **Server-driven kill switch:**
110
+ If the backend returns `kill_switch_active: true` in a heartbeat response, the agent stops uploading new errors (but keeps sending heartbeats so the project stays observable).
111
+
112
+ ## Custom errors
113
+
114
+ ```js
115
+ const sentinelle = require('sentinelle-agent');
116
+
117
+ try {
118
+ await chargeCard(user);
119
+ } catch (err) {
120
+ sentinelle.getAgent()?.reportError({
121
+ type: 'PaymentFailed',
122
+ message: err.message,
123
+ fatal: false,
124
+ });
125
+ throw err;
126
+ }
127
+ ```
128
+
129
+ ## Options
130
+
131
+ ```js
132
+ sentinelle.init({
133
+ apiKey: '...',
134
+ serverUrl: 'https://api.sentinelle.dev',
135
+ appName: 'my-app',
136
+ heartbeatInterval: 60000, // ms, default 60_000
137
+ captureUnhandled: true, // install process-level handlers
138
+ debug: false, // print [Sentinelle Agent] log lines
139
+ });
140
+ ```
141
+
142
+ ## License
143
+
144
+ MIT
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "sentinelle-agent",
3
+ "version": "1.0.0",
4
+ "description": "Sentinelle monitoring agent for Node.js — error capture, route discovery, request metrics, and health reporting. Supports Express, Fastify, Koa, NestJS.",
5
+ "main": "src/index.js",
6
+ "keywords": [
7
+ "monitoring",
8
+ "rasp",
9
+ "security",
10
+ "observability",
11
+ "error-tracking",
12
+ "health-check",
13
+ "express",
14
+ "fastify",
15
+ "koa",
16
+ "nestjs",
17
+ "middleware",
18
+ "sentinelle"
19
+ ],
20
+ "author": "Sentinelle <hello@sentinelle.dev>",
21
+ "license": "MIT",
22
+ "engines": {
23
+ "node": ">=16.0.0"
24
+ },
25
+ "files": [
26
+ "src/",
27
+ "README.md"
28
+ ],
29
+ "homepage": "https://sentinelle.dev",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/sentinelle-dev/sentinelle-agent.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/sentinelle-dev/sentinelle-agent/issues"
36
+ },
37
+ "peerDependencies": {
38
+ "express": ">=4.0.0"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "express": {
42
+ "optional": true
43
+ }
44
+ }
45
+ }
package/src/core.js ADDED
@@ -0,0 +1,320 @@
1
+ /**
2
+ * AgentCore — the heart of the Sentinelle agent
3
+ * Handles heartbeat, error batching, request metrics, and communication with Sentinelle server
4
+ */
5
+
6
+ class AgentCore {
7
+ constructor(config) {
8
+ this.config = config;
9
+ this.running = false;
10
+ this.heartbeatTimer = null;
11
+ this.flushTimer = null;
12
+
13
+ // Buffers
14
+ this.errors = [];
15
+ this.requestMetrics = {
16
+ total: 0,
17
+ byStatus: {},
18
+ byRoute: {},
19
+ avgResponseMs: 0,
20
+ totalResponseMs: 0,
21
+ errors5xx: 0,
22
+ errors4xx: 0,
23
+ };
24
+ this.discoveredRoutes = [];
25
+
26
+ // Server-driven kill switch state — set by heartbeat response.
27
+ // Quand actif, l'agent suspend l'envoi d'erreurs (qui pourraient déclencher
28
+ // des fix auto) mais continue le heartbeat pour rester observable.
29
+ this.killSwitchActive = false;
30
+ this.killSwitchSince = null;
31
+
32
+ // System info
33
+ this.systemInfo = {
34
+ nodeVersion: process.version,
35
+ platform: process.platform,
36
+ arch: process.arch,
37
+ pid: process.pid,
38
+ };
39
+ }
40
+
41
+ log(msg) {
42
+ if (this.config.debug) {
43
+ console.log(`[Sentinelle Agent] ${msg}`);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Start the agent — begins heartbeat and sets up unhandled error capture
49
+ */
50
+ start() {
51
+ if (this.running) return;
52
+ this.running = true;
53
+
54
+ this.log(`Starting agent for "${this.config.appName}" → ${this.config.serverUrl}`);
55
+
56
+ // Heartbeat
57
+ this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), this.config.heartbeatInterval);
58
+
59
+ // Flush error buffer every 30s
60
+ this.flushTimer = setInterval(() => this.flushErrors(), 30000);
61
+
62
+ // Capture unhandled errors
63
+ if (this.config.captureUnhandled) {
64
+ process.on('uncaughtException', (err) => {
65
+ this.reportError({
66
+ type: 'UncaughtException',
67
+ message: err.message,
68
+ stack: err.stack,
69
+ fatal: true,
70
+ timestamp: new Date().toISOString(),
71
+ });
72
+ // Flush immediately for fatal errors
73
+ this.flushErrors();
74
+ });
75
+
76
+ process.on('unhandledRejection', (reason) => {
77
+ const err = reason instanceof Error ? reason : new Error(String(reason));
78
+ this.reportError({
79
+ type: 'UnhandledRejection',
80
+ message: err.message,
81
+ stack: err.stack,
82
+ fatal: false,
83
+ timestamp: new Date().toISOString(),
84
+ });
85
+ });
86
+ }
87
+
88
+ // Send initial heartbeat
89
+ setTimeout(() => this.sendHeartbeat(), 2000);
90
+
91
+ this.log('Agent started');
92
+ }
93
+
94
+ /**
95
+ * Stop the agent
96
+ */
97
+ async stop() {
98
+ this.running = false;
99
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
100
+ if (this.flushTimer) clearInterval(this.flushTimer);
101
+
102
+ // Flush remaining errors
103
+ await this.flushErrors();
104
+
105
+ this.log('Agent stopped');
106
+ }
107
+
108
+ /**
109
+ * Track a request (called by Express middleware)
110
+ */
111
+ trackRequest(data) {
112
+ this.requestMetrics.total++;
113
+ this.requestMetrics.totalResponseMs += data.responseMs || 0;
114
+ this.requestMetrics.avgResponseMs = Math.round(
115
+ this.requestMetrics.totalResponseMs / this.requestMetrics.total
116
+ );
117
+
118
+ // By status code
119
+ const status = data.statusCode || 0;
120
+ this.requestMetrics.byStatus[status] = (this.requestMetrics.byStatus[status] || 0) + 1;
121
+
122
+ if (status >= 500) this.requestMetrics.errors5xx++;
123
+ if (status >= 400 && status < 500) this.requestMetrics.errors4xx++;
124
+
125
+ // By route (top 50)
126
+ const routeKey = `${data.method} ${data.route || data.path}`;
127
+ if (!this.requestMetrics.byRoute[routeKey]) {
128
+ this.requestMetrics.byRoute[routeKey] = { count: 0, totalMs: 0, errors: 0 };
129
+ }
130
+ const route = this.requestMetrics.byRoute[routeKey];
131
+ route.count++;
132
+ route.totalMs += data.responseMs || 0;
133
+ if (status >= 400) route.errors++;
134
+
135
+ // Report slow requests as errors
136
+ if (data.responseMs > 5000) {
137
+ this.reportError({
138
+ type: 'SlowRequest',
139
+ message: `Slow request: ${data.method} ${data.path} took ${data.responseMs}ms`,
140
+ method: data.method,
141
+ path: data.path,
142
+ statusCode: data.statusCode,
143
+ responseMs: data.responseMs,
144
+ timestamp: data.timestamp,
145
+ });
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Report an error (buffered, sent in batches)
151
+ */
152
+ reportError(error) {
153
+ this.errors.push({
154
+ ...error,
155
+ appName: this.config.appName,
156
+ agentVersion: '1.0.0',
157
+ });
158
+
159
+ this.log(`Error captured: ${error.type} — ${error.message}`);
160
+
161
+ // Flush immediately if buffer is large or error is fatal
162
+ if (this.errors.length >= 10 || error.fatal) {
163
+ this.flushErrors();
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Report discovered routes to Sentinelle
169
+ */
170
+ async reportRoutes(routes) {
171
+ this.discoveredRoutes = routes;
172
+ this.log(`Discovered ${routes.length} routes`);
173
+
174
+ try {
175
+ await this._send('/api/webhooks/agent/routes', {
176
+ appName: this.config.appName,
177
+ routes,
178
+ discoveredAt: new Date().toISOString(),
179
+ });
180
+ this.log('Routes reported to Sentinelle');
181
+ } catch (err) {
182
+ this.log(`Failed to report routes: ${err.message}`);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Send heartbeat with system info and metrics
188
+ */
189
+ async sendHeartbeat() {
190
+ if (!this.running) return;
191
+
192
+ const memUsage = process.memoryUsage();
193
+ const uptime = process.uptime();
194
+
195
+ const payload = {
196
+ appName: this.config.appName,
197
+ status: 'alive',
198
+ uptime: Math.round(uptime),
199
+ memory: {
200
+ rss: Math.round(memUsage.rss / 1048576), // MB
201
+ heapUsed: Math.round(memUsage.heapUsed / 1048576),
202
+ heapTotal: Math.round(memUsage.heapTotal / 1048576),
203
+ },
204
+ system: this.systemInfo,
205
+ metrics: {
206
+ totalRequests: this.requestMetrics.total,
207
+ avgResponseMs: this.requestMetrics.avgResponseMs,
208
+ errors5xx: this.requestMetrics.errors5xx,
209
+ errors4xx: this.requestMetrics.errors4xx,
210
+ topRoutes: this._getTopRoutes(5),
211
+ statusDistribution: this.requestMetrics.byStatus,
212
+ },
213
+ routeCount: this.discoveredRoutes.length,
214
+ timestamp: new Date().toISOString(),
215
+ };
216
+
217
+ try {
218
+ const response = await this._send('/api/webhooks/agent/heartbeat', payload);
219
+ this.log('Heartbeat sent');
220
+ this._applyServerDirectives(response);
221
+ } catch (err) {
222
+ this.log(`Heartbeat failed: ${err.message}`);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Applique les directives renvoyées par le backend (kill switch, etc.)
228
+ */
229
+ _applyServerDirectives(response) {
230
+ if (!response || typeof response !== 'object') return;
231
+ const wasActive = this.killSwitchActive;
232
+ this.killSwitchActive = !!response.kill_switch_active;
233
+ if (this.killSwitchActive && !wasActive) {
234
+ this.killSwitchSince = new Date().toISOString();
235
+ this.log('🛑 Kill switch activé par le serveur — pause envoi d\'erreurs');
236
+ } else if (!this.killSwitchActive && wasActive) {
237
+ this.killSwitchSince = null;
238
+ this.log('Kill switch relâché — reprise normale');
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Flush error buffer to Sentinelle
244
+ */
245
+ async flushErrors() {
246
+ if (this.errors.length === 0) return;
247
+
248
+ if (this.killSwitchActive) {
249
+ // On garde les erreurs en buffer (cap à 1000 pour éviter fuite mémoire)
250
+ // et on attend la levée du kill switch.
251
+ if (this.errors.length > 1000) {
252
+ this.errors.splice(0, this.errors.length - 1000);
253
+ }
254
+ this.log(`Kill switch actif — ${this.errors.length} erreur(s) en attente`);
255
+ return;
256
+ }
257
+
258
+ const batch = this.errors.splice(0, this.errors.length);
259
+
260
+ try {
261
+ await this._send('/api/webhooks/agent/errors', {
262
+ appName: this.config.appName,
263
+ errors: batch,
264
+ timestamp: new Date().toISOString(),
265
+ });
266
+ this.log(`Flushed ${batch.length} errors`);
267
+ } catch (err) {
268
+ // Put errors back if send failed
269
+ this.errors.unshift(...batch);
270
+ this.log(`Error flush failed: ${err.message}`);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Get top N routes by request count
276
+ */
277
+ _getTopRoutes(n) {
278
+ return Object.entries(this.requestMetrics.byRoute)
279
+ .map(([route, data]) => ({
280
+ route,
281
+ count: data.count,
282
+ avgMs: Math.round(data.totalMs / data.count),
283
+ errorRate: data.count > 0 ? Math.round((data.errors / data.count) * 100) : 0,
284
+ }))
285
+ .sort((a, b) => b.count - a.count)
286
+ .slice(0, n);
287
+ }
288
+
289
+ /**
290
+ * Send data to Sentinelle server
291
+ */
292
+ async _send(path, data) {
293
+ const url = this.config.serverUrl + path;
294
+ const controller = new AbortController();
295
+ const timeout = setTimeout(() => controller.abort(), 10000);
296
+
297
+ try {
298
+ const res = await fetch(url, {
299
+ method: 'POST',
300
+ headers: {
301
+ 'Content-Type': 'application/json',
302
+ 'X-Sentinelle-Key': this.config.apiKey,
303
+ 'User-Agent': `sentinelle-agent/1.0.0 (${this.config.appName})`,
304
+ },
305
+ body: JSON.stringify(data),
306
+ signal: controller.signal,
307
+ });
308
+
309
+ if (!res.ok) {
310
+ throw new Error(`HTTP ${res.status}`);
311
+ }
312
+
313
+ return await res.json();
314
+ } finally {
315
+ clearTimeout(timeout);
316
+ }
317
+ }
318
+ }
319
+
320
+ module.exports = { AgentCore };
package/src/express.js ADDED
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Express.js integration — middleware, error handler, and route discovery
3
+ */
4
+
5
+ /**
6
+ * Request tracking middleware — captures response time, status codes, and request metadata
7
+ * Add BEFORE your routes: app.use(sentinelle.middleware())
8
+ */
9
+ const expressMiddleware = (agent) => {
10
+ return (req, res, next) => {
11
+ const start = process.hrtime.bigint();
12
+
13
+ // Capture response finish
14
+ const originalEnd = res.end;
15
+ res.end = function (...args) {
16
+ const end = process.hrtime.bigint();
17
+ const responseMs = Number(end - start) / 1_000_000;
18
+
19
+ // Track request
20
+ agent.trackRequest({
21
+ method: req.method,
22
+ path: req.originalUrl || req.url,
23
+ route: req.route?.path || req.originalUrl || req.url,
24
+ statusCode: res.statusCode,
25
+ responseMs: Math.round(responseMs),
26
+ ip: req.ip || req.connection?.remoteAddress,
27
+ userAgent: req.headers['user-agent'],
28
+ timestamp: new Date().toISOString(),
29
+ });
30
+
31
+ originalEnd.apply(this, args);
32
+ };
33
+
34
+ next();
35
+ };
36
+ };
37
+
38
+ /**
39
+ * Error handler middleware — captures unhandled errors in routes
40
+ * Add AFTER your routes: app.use(sentinelle.errorHandler())
41
+ */
42
+ const expressErrorHandler = (agent) => {
43
+ return (err, req, res, next) => {
44
+ // Report the error to Sentinelle
45
+ agent.reportError({
46
+ type: err.name || 'Error',
47
+ message: err.message,
48
+ stack: err.stack,
49
+ method: req.method,
50
+ path: req.originalUrl || req.url,
51
+ route: req.route?.path,
52
+ statusCode: err.status || err.statusCode || 500,
53
+ ip: req.ip || req.connection?.remoteAddress,
54
+ userAgent: req.headers['user-agent'],
55
+ body: req.method !== 'GET' ? sanitizeBody(req.body) : undefined,
56
+ query: Object.keys(req.query || {}).length > 0 ? req.query : undefined,
57
+ timestamp: new Date().toISOString(),
58
+ });
59
+
60
+ // Don't swallow the error — pass it to the next handler
61
+ next(err);
62
+ };
63
+ };
64
+
65
+ /**
66
+ * Sanitize request body — remove sensitive fields
67
+ */
68
+ const sanitizeBody = (body) => {
69
+ if (!body || typeof body !== 'object') return undefined;
70
+
71
+ const sensitiveKeys = ['password', 'token', 'secret', 'authorization', 'cookie', 'credit_card', 'card_number', 'cvv', 'ssn'];
72
+ const sanitized = { ...body };
73
+
74
+ for (const key of Object.keys(sanitized)) {
75
+ if (sensitiveKeys.some(s => key.toLowerCase().includes(s))) {
76
+ sanitized[key] = '[REDACTED]';
77
+ }
78
+ }
79
+
80
+ return sanitized;
81
+ };
82
+
83
+ /**
84
+ * Discover all registered Express routes
85
+ * Call after all routes are registered: sentinelle.discoverRoutes(app)
86
+ */
87
+ const discoverExpressRoutes = (app) => {
88
+ const routes = [];
89
+
90
+ const extractRoutes = (stack, prefix = '') => {
91
+ if (!stack) return;
92
+
93
+ for (const layer of stack) {
94
+ if (layer.route) {
95
+ // Direct route
96
+ const methods = Object.keys(layer.route.methods)
97
+ .filter(m => layer.route.methods[m])
98
+ .map(m => m.toUpperCase());
99
+
100
+ for (const method of methods) {
101
+ routes.push({
102
+ method,
103
+ path: prefix + layer.route.path,
104
+ });
105
+ }
106
+ } else if (layer.name === 'router' && layer.handle?.stack) {
107
+ // Nested router
108
+ const routerPrefix = layer.regexp
109
+ ? extractPrefix(layer.regexp)
110
+ : '';
111
+ extractRoutes(layer.handle.stack, prefix + routerPrefix);
112
+ }
113
+ }
114
+ };
115
+
116
+ // Try to access Express internal router stack
117
+ if (app._router?.stack) {
118
+ extractRoutes(app._router.stack);
119
+ }
120
+
121
+ return routes;
122
+ };
123
+
124
+ /**
125
+ * Extract prefix path from Express regex
126
+ */
127
+ const extractPrefix = (regexp) => {
128
+ if (!regexp) return '';
129
+ const str = regexp.toString();
130
+
131
+ // Match patterns like /^\/api\/?(?=\/|$)/i
132
+ const match = str.match(/^\/\^(\\\/[^?*+{(|]+)/);
133
+ if (match) {
134
+ return match[1].replace(/\\\//g, '/');
135
+ }
136
+
137
+ return '';
138
+ };
139
+
140
+ module.exports = { expressMiddleware, expressErrorHandler, discoverExpressRoutes };
package/src/index.js ADDED
@@ -0,0 +1,95 @@
1
+ const { AgentCore } = require('./core');
2
+ const { expressMiddleware, expressErrorHandler, discoverExpressRoutes } = require('./express');
3
+
4
+ /**
5
+ * Sentinelle Agent — install in your Node.js app for automatic monitoring
6
+ *
7
+ * Usage:
8
+ * const devguard = require('sentinelle-agent');
9
+ *
10
+ * // Initialize
11
+ * sentinelle.init({
12
+ * apiKey: 'your-project-api-key',
13
+ * serverUrl: 'http://localhost:4000', // Sentinelle server
14
+ * appName: 'my-app',
15
+ * });
16
+ *
17
+ * // Express middleware (add BEFORE routes)
18
+ * app.use(sentinelle.middleware());
19
+ *
20
+ * // Express error handler (add AFTER routes)
21
+ * app.use(sentinelle.errorHandler());
22
+ *
23
+ * // Auto-discover routes (call after all routes are registered)
24
+ * sentinelle.discoverRoutes(app);
25
+ */
26
+
27
+ let agent = null;
28
+
29
+ const init = (options = {}) => {
30
+ if (agent) {
31
+ console.warn('[Sentinelle] Agent already initialized');
32
+ return agent;
33
+ }
34
+
35
+ const config = {
36
+ apiKey: options.apiKey || process.env.SENTINELLE_API_KEY,
37
+ serverUrl: (options.serverUrl || process.env.SENTINELLE_SERVER_URL || 'http://localhost:4000').replace(/\/$/, ''),
38
+ appName: options.appName || process.env.SENTINELLE_APP_NAME || 'unknown',
39
+ heartbeatInterval: options.heartbeatInterval || 60000, // 1 min
40
+ captureUnhandled: options.captureUnhandled !== false,
41
+ debug: options.debug || false,
42
+ };
43
+
44
+ if (!config.apiKey) {
45
+ console.error('[Sentinelle] API key required. Set apiKey option or SENTINELLE_API_KEY env var.');
46
+ return null;
47
+ }
48
+
49
+ agent = new AgentCore(config);
50
+ agent.start();
51
+
52
+ return agent;
53
+ };
54
+
55
+ const middleware = () => {
56
+ return (req, res, next) => {
57
+ if (!agent) return next();
58
+ return expressMiddleware(agent)(req, res, next);
59
+ };
60
+ };
61
+
62
+ const errorHandler = () => {
63
+ return (err, req, res, next) => {
64
+ if (!agent) return next(err);
65
+ return expressErrorHandler(agent)(err, req, res, next);
66
+ };
67
+ };
68
+
69
+ const discoverRoutes = (app) => {
70
+ if (!agent) {
71
+ console.warn('[Sentinelle] Agent not initialized. Call sentinelle.init() first.');
72
+ return;
73
+ }
74
+ const routes = discoverExpressRoutes(app);
75
+ agent.reportRoutes(routes);
76
+ return routes;
77
+ };
78
+
79
+ const getAgent = () => agent;
80
+
81
+ const shutdown = async () => {
82
+ if (agent) {
83
+ await agent.stop();
84
+ agent = null;
85
+ }
86
+ };
87
+
88
+ module.exports = {
89
+ init,
90
+ middleware,
91
+ errorHandler,
92
+ discoverRoutes,
93
+ getAgent,
94
+ shutdown,
95
+ };