@ninenity/neutrun 1.2.0 → 1.9.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.
Files changed (73) hide show
  1. package/README.md +2 -2
  2. package/dist/CLI/Localize.d.ts +6 -0
  3. package/dist/CLI/Localize.d.ts.map +1 -1
  4. package/dist/CLI/Localize.js +63 -14
  5. package/dist/CLI/Localize.js.map +1 -1
  6. package/dist/Shared/Client.d.ts +1 -0
  7. package/dist/Shared/Client.d.ts.map +1 -1
  8. package/dist/Shared/Client.js +69 -2
  9. package/dist/Shared/Client.js.map +1 -1
  10. package/dist/Shared/ComponentRuntime.d.ts.map +1 -1
  11. package/dist/Shared/ComponentRuntime.js +2 -0
  12. package/dist/Shared/ComponentRuntime.js.map +1 -1
  13. package/dist/Shared/Config.js +2 -2
  14. package/dist/Shared/Config.js.map +1 -1
  15. package/dist/Shared/Definer.d.ts.map +1 -1
  16. package/dist/Shared/Definer.js +2 -0
  17. package/dist/Shared/Definer.js.map +1 -1
  18. package/dist/Shared/Environment.d.ts +10 -0
  19. package/dist/Shared/Environment.d.ts.map +1 -0
  20. package/dist/Shared/Environment.js +18 -0
  21. package/dist/Shared/Environment.js.map +1 -0
  22. package/dist/Shared/ExpressApi.d.ts +60 -1
  23. package/dist/Shared/ExpressApi.d.ts.map +1 -1
  24. package/dist/Shared/ExpressApi.js +393 -29
  25. package/dist/Shared/ExpressApi.js.map +1 -1
  26. package/dist/Shared/InteractionResponse.d.ts +12 -0
  27. package/dist/Shared/InteractionResponse.d.ts.map +1 -0
  28. package/dist/Shared/InteractionResponse.js +72 -0
  29. package/dist/Shared/InteractionResponse.js.map +1 -0
  30. package/dist/Shared/Neutrun.d.ts +1 -0
  31. package/dist/Shared/Neutrun.d.ts.map +1 -1
  32. package/dist/Shared/Neutrun.js +13 -5
  33. package/dist/Shared/Neutrun.js.map +1 -1
  34. package/dist/Shared/NinenityApi.d.ts +54 -1
  35. package/dist/Shared/NinenityApi.d.ts.map +1 -1
  36. package/dist/Shared/NinenityApi.js +421 -18
  37. package/dist/Shared/NinenityApi.js.map +1 -1
  38. package/dist/Shared/Observability.d.ts +59 -0
  39. package/dist/Shared/Observability.d.ts.map +1 -0
  40. package/dist/Shared/Observability.js +184 -0
  41. package/dist/Shared/Observability.js.map +1 -0
  42. package/dist/Shared/ProcessManager.d.ts +1 -0
  43. package/dist/Shared/ProcessManager.d.ts.map +1 -1
  44. package/dist/Shared/ProcessManager.js +21 -6
  45. package/dist/Shared/ProcessManager.js.map +1 -1
  46. package/dist/Shared/ServerMetricsService.d.ts +85 -0
  47. package/dist/Shared/ServerMetricsService.d.ts.map +1 -0
  48. package/dist/Shared/ServerMetricsService.js +284 -0
  49. package/dist/Shared/ServerMetricsService.js.map +1 -0
  50. package/dist/Shared/TelemetryPacket.d.ts +28 -0
  51. package/dist/Shared/TelemetryPacket.d.ts.map +1 -0
  52. package/dist/Shared/TelemetryPacket.js +22 -0
  53. package/dist/Shared/TelemetryPacket.js.map +1 -0
  54. package/dist/Shared/TelemetryService.d.ts +34 -0
  55. package/dist/Shared/TelemetryService.d.ts.map +1 -1
  56. package/dist/Shared/TelemetryService.js +235 -19
  57. package/dist/Shared/TelemetryService.js.map +1 -1
  58. package/dist/Shared/WebSocket.d.ts.map +1 -1
  59. package/dist/Shared/WebSocket.js +39 -9
  60. package/dist/Shared/WebSocket.js.map +1 -1
  61. package/dist/api.d.ts +2 -2
  62. package/dist/api.d.ts.map +1 -1
  63. package/dist/api.js +4 -1
  64. package/dist/api.js.map +1 -1
  65. package/dist/express.d.ts +2 -0
  66. package/dist/express.d.ts.map +1 -1
  67. package/dist/express.js +4 -1
  68. package/dist/express.js.map +1 -1
  69. package/dist/index.d.ts +12 -4
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +18 -2
  72. package/dist/index.js.map +1 -1
  73. package/package.json +10 -2
@@ -10,6 +10,7 @@ const node_crypto_1 = require("node:crypto");
10
10
  const ConsoleLog_1 = require("./ConsoleLog");
11
11
  const ProcessManager_1 = __importDefault(require("./ProcessManager"));
12
12
  const TelemetryService_1 = require("./TelemetryService");
13
+ const Observability_1 = __importDefault(require("./Observability"));
13
14
  class ApiRequestError extends Error {
14
15
  status;
15
16
  data;
@@ -25,13 +26,18 @@ class ApiRequestError extends Error {
25
26
  exports.ApiRequestError = ApiRequestError;
26
27
  const DEFAULT_HOST = '127.0.0.1';
27
28
  const DEFAULT_PORT = 3000;
28
- const DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
29
+ const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
29
30
  const DEFAULT_REQUEST_TIMEOUT_MS = 30 * 1000;
30
31
  const DEFAULT_RATE_LIMIT_WINDOW_MS = 60 * 1000;
31
32
  const DEFAULT_RATE_LIMIT_MAX = 60;
32
33
  const MAX_RATE_LIMIT_ENTRIES = 10_000;
33
34
  const SERVER_CLOSE_TIMEOUT_MS = 5_000;
34
35
  const MAX_INTERNAL_RESPONSE_BYTES = 2 * 1024 * 1024;
36
+ const DEFAULT_MAX_HEADER_BYTES = 16 * 1024;
37
+ const DEFAULT_MAX_QUERY_PARAMETERS = 100;
38
+ const DEFAULT_MAX_JSON_DEPTH = 16;
39
+ const DEFAULT_MAX_JSON_KEYS = 1_000;
40
+ const REQUEST_ID_PATTERN = /^[a-zA-Z0-9._:-]{8,100}$/;
35
41
  class ExpressApiService {
36
42
  app;
37
43
  settings = normalizeSettings(undefined);
@@ -59,6 +65,11 @@ class ExpressApiService {
59
65
  this.app.stop = () => this.stop();
60
66
  this.app.fetch = (path, options) => this.fetch(path, options);
61
67
  this.app.fetchJson = (path, options) => this.fetchJson(path, options);
68
+ this.app.protect = (options) => this.protectionMiddleware(options);
69
+ this.app.validate = (options) => this.validationMiddleware(options);
70
+ this.app.getIdentity = (response) => response.locals.neutrunIdentity;
71
+ this.app.getStatus = () => this.getStatus();
72
+ TelemetryService_1.Telemetry.setApiStatusProvider(() => this.getTelemetryStatus());
62
73
  Object.defineProperties(this.app, {
63
74
  isRunning: {
64
75
  enumerable: true,
@@ -95,6 +106,19 @@ class ExpressApiService {
95
106
  return;
96
107
  this.installBuiltInMiddleware();
97
108
  }
109
+ getStatus() {
110
+ return {
111
+ configured: this.configured,
112
+ enabled: this.settings.enabled,
113
+ running: this.server !== null,
114
+ suspended: this.resourcesSuspended,
115
+ hibernating: this.isHibernating,
116
+ lifecycle: this.settings.lifecycle.mode,
117
+ host: this.settings.host,
118
+ port: this.settings.port,
119
+ url: this.url
120
+ };
121
+ }
98
122
  finalize() {
99
123
  if (this.finalized)
100
124
  return;
@@ -131,7 +155,9 @@ class ExpressApiService {
131
155
  validateSettings(this.settings);
132
156
  this.finalize();
133
157
  this.startPromise = new Promise((resolve, reject) => {
134
- const server = (0, node_http_1.createServer)(this.app);
158
+ const server = (0, node_http_1.createServer)({
159
+ maxHeaderSize: clampInteger(this.settings.security.maxHeaderBytes, 8 * 1024, 64 * 1024, DEFAULT_MAX_HEADER_BYTES)
160
+ }, this.app);
135
161
  const requestTimeout = this.settings.security.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
136
162
  server.requestTimeout = requestTimeout;
137
163
  server.headersTimeout = Math.max(requestTimeout + 1_000, 5_000);
@@ -197,7 +223,7 @@ class ExpressApiService {
197
223
  await this.ensureStarted();
198
224
  const url = this.resolveUrl(path);
199
225
  const headers = new Headers(options.headers);
200
- const token = this.resolveToken();
226
+ const token = this.resolveCredentials()[0]?.token ?? '';
201
227
  const authentication = this.settings.security.authentication;
202
228
  if (token && authentication !== 'none' && !headers.has('authorization') && !headers.has('x-api-key')) {
203
229
  if (authentication === 'api-key')
@@ -205,6 +231,9 @@ class ExpressApiService {
205
231
  else
206
232
  headers.set('authorization', `Bearer ${token}`);
207
233
  }
234
+ if (!headers.has('x-request-id')) {
235
+ headers.set('x-request-id', Observability_1.default.getCorrelationId() ?? (0, node_crypto_1.randomUUID)());
236
+ }
208
237
  const body = serializeBody(options.body, headers);
209
238
  const { query, ...init } = options;
210
239
  const requestInit = {
@@ -225,9 +254,11 @@ class ExpressApiService {
225
254
  }
226
255
  installBuiltInMiddleware() {
227
256
  this.middlewareInstalled = true;
257
+ this.app.use(this.requestContextMiddleware());
228
258
  this.app.use(this.securityHeaders());
229
259
  this.app.use(this.corsMiddleware());
230
260
  this.app.use(this.requestTrackingMiddleware());
261
+ this.app.use(this.requestShapeLimitsMiddleware());
231
262
  this.app.use(this.requestTimeoutMiddleware());
232
263
  this.app.use(this.rateLimitMiddleware());
233
264
  this.app.use(this.authenticationMiddleware());
@@ -237,6 +268,16 @@ class ExpressApiService {
237
268
  extended: true,
238
269
  limit: this.settings.security.bodyLimit ?? '1mb'
239
270
  }));
271
+ this.app.use(this.jsonComplexityMiddleware());
272
+ }
273
+ requestContextMiddleware() {
274
+ return (req, res, next) => {
275
+ const incoming = req.get('x-request-id')?.trim() || '';
276
+ const requestId = REQUEST_ID_PATTERN.test(incoming) ? incoming : (0, node_crypto_1.randomUUID)();
277
+ res.locals.neutrunRequestId = requestId;
278
+ res.setHeader('X-Request-Id', requestId);
279
+ Observability_1.default.run({ correlationId: requestId, requestId, source: 'express-api' }, () => next());
280
+ };
240
281
  }
241
282
  securityHeaders() {
242
283
  return (_req, res, next) => {
@@ -244,6 +285,9 @@ class ExpressApiService {
244
285
  res.setHeader('X-Frame-Options', 'DENY');
245
286
  res.setHeader('Referrer-Policy', 'no-referrer');
246
287
  res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
288
+ res.setHeader('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'; base-uri 'none'");
289
+ res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()');
290
+ res.setHeader('Cache-Control', 'no-store');
247
291
  next();
248
292
  };
249
293
  }
@@ -260,8 +304,8 @@ class ExpressApiService {
260
304
  : undefined;
261
305
  if (allowed) {
262
306
  res.setHeader('Access-Control-Allow-Origin', allowed);
263
- res.setHeader('Vary', 'Origin');
264
- res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, X-API-Key');
307
+ appendVaryHeader(res, 'Origin');
308
+ res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, X-API-Key, X-Request-Id');
265
309
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
266
310
  }
267
311
  if (req.method === 'OPTIONS')
@@ -271,6 +315,7 @@ class ExpressApiService {
271
315
  }
272
316
  requestTrackingMiddleware() {
273
317
  return (req, res, next) => {
318
+ const startedAt = performance.now();
274
319
  this.lastActivityAt = Date.now();
275
320
  this.activeRequests += 1;
276
321
  let completed = false;
@@ -281,13 +326,49 @@ class ExpressApiService {
281
326
  this.activeRequests = Math.max(0, this.activeRequests - 1);
282
327
  this.lastActivityAt = Date.now();
283
328
  this.scheduleIdleCheck();
284
- this.recordRequest(req, res, res.statusCode);
329
+ this.recordRequest(req, res, res.statusCode, performance.now() - startedAt, !res.writableEnded);
285
330
  };
286
331
  res.once('finish', complete);
287
332
  res.once('close', complete);
288
333
  next();
289
334
  };
290
335
  }
336
+ requestShapeLimitsMiddleware() {
337
+ return (req, res, next) => {
338
+ const maximum = clampInteger(this.settings.security.maxQueryParameters, 1, 1_000, DEFAULT_MAX_QUERY_PARAMETERS);
339
+ if (countObjectKeys(req.query, maximum + 1) > maximum) {
340
+ this.emitAudit(req, res, {
341
+ type: 'validation',
342
+ outcome: 'denied',
343
+ status: 400,
344
+ reason: 'query_limit'
345
+ });
346
+ return res.status(400).json({ success: false, error: 'Too many query parameters' });
347
+ }
348
+ next();
349
+ };
350
+ }
351
+ jsonComplexityMiddleware() {
352
+ return (req, res, next) => {
353
+ if (req.body === undefined || req.body === null || typeof req.body !== 'object')
354
+ return next();
355
+ const maxDepth = clampInteger(this.settings.security.maxJsonDepth, 1, 100, DEFAULT_MAX_JSON_DEPTH);
356
+ const maxKeys = clampInteger(this.settings.security.maxJsonKeys, 1, 100_000, DEFAULT_MAX_JSON_KEYS);
357
+ const complexity = inspectObjectComplexity(req.body, maxDepth, maxKeys);
358
+ if (complexity.valid)
359
+ return next();
360
+ this.emitAudit(req, res, {
361
+ type: 'validation',
362
+ outcome: 'denied',
363
+ status: 400,
364
+ reason: complexity.reason
365
+ });
366
+ return res.status(400).json({
367
+ success: false,
368
+ error: 'Request body is too complex'
369
+ });
370
+ };
371
+ }
291
372
  requestTimeoutMiddleware() {
292
373
  return (_req, res, next) => {
293
374
  const timeoutMs = this.settings.security.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
@@ -320,11 +401,21 @@ class ExpressApiService {
320
401
  const current = this.rateLimitEntries.get(key);
321
402
  if (!current || now - current.startedAt >= windowMs) {
322
403
  this.rateLimitEntries.set(key, { startedAt: now, count: 1 });
404
+ setRateLimitHeaders(res, max, max - 1, windowMs);
323
405
  return next();
324
406
  }
325
- current.count += 1;
407
+ current.count = Math.min(Number.MAX_SAFE_INTEGER, current.count + 1);
408
+ const remaining = Math.max(0, max - current.count);
409
+ const resetMs = Math.max(0, windowMs - (now - current.startedAt));
410
+ setRateLimitHeaders(res, max, remaining, resetMs);
326
411
  if (current.count > max) {
327
- res.setHeader('Retry-After', Math.ceil((windowMs - (now - current.startedAt)) / 1_000));
412
+ res.setHeader('Retry-After', Math.ceil(resetMs / 1_000));
413
+ Observability_1.default.increment('neutrun_api_rate_limit_total');
414
+ this.emitAudit(req, res, {
415
+ type: 'rate-limit',
416
+ outcome: 'denied',
417
+ status: 429
418
+ });
328
419
  return res.status(429).json({
329
420
  success: false,
330
421
  error: 'Too Many Requests'
@@ -336,27 +427,118 @@ class ExpressApiService {
336
427
  authenticationMiddleware() {
337
428
  return (req, res, next) => {
338
429
  const authentication = this.settings.security.authentication;
339
- const token = this.resolveToken();
340
- if (authentication === 'none' || !token || isPublicPath(req.path, this.settings.security.publicPaths)) {
430
+ if (authentication === 'none') {
431
+ res.locals.neutrunIdentity = anonymousIdentity();
432
+ return next();
433
+ }
434
+ if (isPublicPath(req.path, this.settings.security.publicPaths)) {
435
+ res.locals.neutrunIdentity = anonymousIdentity();
341
436
  return next();
342
437
  }
343
438
  const received = authentication === 'api-key'
344
439
  ? req.get('x-api-key')
345
440
  : extractBearerToken(req.get('authorization'));
346
- if (!received || !timingSafeEqual(received, token)) {
441
+ const credential = received
442
+ ? this.resolveCredentials().find((entry) => timingSafeEqual(received, entry.token))
443
+ : undefined;
444
+ if (!credential) {
445
+ Observability_1.default.increment('neutrun_api_authentication_total', 1, { outcome: 'denied' });
446
+ this.emitAudit(req, res, {
447
+ type: 'authentication',
448
+ outcome: 'denied',
449
+ status: 401,
450
+ reason: received ? 'invalid_credential' : 'missing_credential'
451
+ });
347
452
  return res.status(401).json({
348
453
  success: false,
349
454
  error: 'Unauthorized'
350
455
  });
351
456
  }
457
+ const identity = {
458
+ authentication,
459
+ credentialId: credential.id,
460
+ roles: credential.roles,
461
+ scopes: credential.scopes
462
+ };
463
+ res.locals.neutrunIdentity = identity;
464
+ Observability_1.default.increment('neutrun_api_authentication_total', 1, { outcome: 'allowed' });
465
+ this.emitAudit(req, res, {
466
+ type: 'authentication',
467
+ outcome: 'allowed',
468
+ credentialId: credential.id
469
+ });
352
470
  next();
353
471
  };
354
472
  }
355
- resolveToken() {
356
- const configured = this.settings.security.token?.trim();
357
- if (configured)
358
- return configured;
359
- return process.env[this.settings.security.tokenEnv]?.trim() || '';
473
+ protectionMiddleware(options = {}) {
474
+ return (req, res, next) => {
475
+ const run = async () => {
476
+ const identity = res.locals.neutrunIdentity;
477
+ const roles = new Set(identity?.roles ?? []);
478
+ const scopes = new Set(identity?.scopes ?? []);
479
+ const hasRoles = (options.roles ?? []).every((role) => roles.has(role));
480
+ const hasScopes = (options.scopes ?? []).every((scope) => scopes.has(scope));
481
+ const policyEligible = Boolean(identity && identity.authentication !== 'none' && hasRoles && hasScopes);
482
+ const customAllowed = policyEligible && options.authorize
483
+ ? await options.authorize(req, identity)
484
+ : policyEligible;
485
+ if (policyEligible && identity && customAllowed) {
486
+ this.emitAudit(req, res, {
487
+ type: 'authorization',
488
+ outcome: 'allowed',
489
+ credentialId: identity.credentialId
490
+ });
491
+ next();
492
+ return;
493
+ }
494
+ Observability_1.default.increment('neutrun_api_authorization_total', 1, { outcome: 'denied' });
495
+ this.emitAudit(req, res, {
496
+ type: 'authorization',
497
+ outcome: 'denied',
498
+ status: identity?.authentication === 'none' ? 401 : 403,
499
+ credentialId: identity?.credentialId,
500
+ reason: 'policy_denied'
501
+ });
502
+ res.status(identity?.authentication === 'none' ? 401 : 403).json({
503
+ success: false,
504
+ error: identity?.authentication === 'none' ? 'Unauthorized' : 'Forbidden'
505
+ });
506
+ };
507
+ void run().catch(next);
508
+ };
509
+ }
510
+ validationMiddleware(options) {
511
+ return (req, res, next) => {
512
+ const run = async () => {
513
+ const checks = [
514
+ ['body', options.body, req.body],
515
+ ['query', options.query, req.query],
516
+ ['params', options.params, req.params]
517
+ ];
518
+ for (const [part, validator, value] of checks) {
519
+ if (!validator)
520
+ continue;
521
+ const result = await validator(value, req);
522
+ if (result === false || typeof result === 'string') {
523
+ const reason = typeof result === 'string' ? result.slice(0, 200) : `Invalid ${part}`;
524
+ Observability_1.default.increment('neutrun_api_validation_total', 1, { outcome: 'denied', part });
525
+ this.emitAudit(req, res, {
526
+ type: 'validation',
527
+ outcome: 'denied',
528
+ status: 400,
529
+ reason: part
530
+ });
531
+ res.status(400).json({ success: false, error: reason });
532
+ return;
533
+ }
534
+ }
535
+ next();
536
+ };
537
+ void run().catch(next);
538
+ };
539
+ }
540
+ resolveCredentials() {
541
+ return resolveCredentials(this.settings.security);
360
542
  }
361
543
  scheduleIdleCheck() {
362
544
  this.clearIdleTimer();
@@ -431,13 +613,42 @@ class ExpressApiService {
431
613
  this.cleanupRegistered = true;
432
614
  ProcessManager_1.default.registerCleanupTask(() => this.stop(), 'mini API Express');
433
615
  }
434
- recordRequest(req, res, status, error = false) {
435
- const path = req.path || req.originalUrl || '/';
616
+ recordRequest(req, res, status, durationMs, error = false) {
617
+ const path = normalizeTelemetryPath(req.route?.path ?? req.path ?? '/');
436
618
  TelemetryService_1.Telemetry.registerCounter('apiRequests');
437
619
  TelemetryService_1.Telemetry.registerEvent(`api:${req.method}:${path}`.slice(0, 120), {
438
- error: error || status >= 400
620
+ error: error || status >= 400,
621
+ durationMs
622
+ });
623
+ Observability_1.default.increment('neutrun_api_requests_total', 1, {
624
+ method: req.method,
625
+ outcome: status >= 400 || error ? 'error' : 'success',
626
+ status: `${Math.floor(status / 100)}xx`
627
+ });
628
+ Observability_1.default.observe('neutrun_api_request_duration_ms', durationMs, {
629
+ method: req.method
630
+ });
631
+ this.emitAudit(req, res, {
632
+ type: 'request',
633
+ outcome: status >= 400 || error ? 'error' : 'success',
634
+ status,
635
+ durationMs
636
+ });
637
+ }
638
+ emitAudit(req, res, event) {
639
+ const audit = this.settings.security.audit;
640
+ if (!audit)
641
+ return;
642
+ const payload = {
643
+ ...event,
644
+ at: new Date().toISOString(),
645
+ requestId: String(res.locals.neutrunRequestId || (0, node_crypto_1.randomUUID)()),
646
+ method: req.method.slice(0, 16),
647
+ path: normalizeTelemetryPath(req.route?.path ?? req.path ?? '/')
648
+ };
649
+ void Promise.resolve(audit(payload)).catch((error) => {
650
+ (0, ConsoleLog_1.logWarn)(`Falha no destino de auditoria da mini API: ${error instanceof Error ? error.message : error}`, 'Api');
439
651
  });
440
- void res;
441
652
  }
442
653
  resolveUrl(path) {
443
654
  const base = this.url;
@@ -464,6 +675,22 @@ class ExpressApiService {
464
675
  const port = typeof address === 'object' && address ? address.port : this.settings.port;
465
676
  return this.publicUrl(port);
466
677
  }
678
+ get isHibernating() {
679
+ return Boolean(this.configured &&
680
+ this.settings.enabled &&
681
+ this.settings.lifecycle.mode !== 'always' &&
682
+ (!this.server || this.resourcesSuspended));
683
+ }
684
+ getTelemetryStatus() {
685
+ return {
686
+ configured: this.configured,
687
+ enabled: this.settings.enabled,
688
+ running: this.server !== null,
689
+ suspended: this.resourcesSuspended,
690
+ hibernating: this.isHibernating,
691
+ lifecycle: this.settings.lifecycle.mode
692
+ };
693
+ }
467
694
  clearIdleTimer() {
468
695
  if (!this.idleTimer)
469
696
  return;
@@ -511,32 +738,169 @@ function normalizeSettings(options) {
511
738
  onWake: lifecycle.onWake ?? serverlessOptions.onWake
512
739
  },
513
740
  security: {
514
- authentication: api.security?.authentication ?? (resolveToken(api.security) || isPublicHost(host) ? 'bearer' : 'none'),
741
+ authentication: api.security?.authentication ?? (resolveCredentials(api.security).length > 0 || isPublicHost(host) ? 'bearer' : 'none'),
515
742
  token: api.security?.token,
516
743
  tokenEnv: api.security?.tokenEnv ?? 'NEUTRUN_API_TOKEN',
744
+ tokens: api.security?.tokens,
745
+ tokensEnv: api.security?.tokensEnv ?? 'NEUTRUN_API_TOKENS',
746
+ credentials: api.security?.credentials,
747
+ allowInsecurePublic: api.security?.allowInsecurePublic,
517
748
  publicPaths: api.security?.publicPaths ?? [],
518
749
  cors: api.security?.cors,
519
750
  bodyLimit: api.security?.bodyLimit,
520
751
  rateLimit: api.security?.rateLimit,
521
752
  requestTimeoutMs: api.security?.requestTimeoutMs,
522
- trustProxy: api.security?.trustProxy
753
+ trustProxy: api.security?.trustProxy,
754
+ maxHeaderBytes: api.security?.maxHeaderBytes,
755
+ maxQueryParameters: api.security?.maxQueryParameters,
756
+ maxJsonDepth: api.security?.maxJsonDepth,
757
+ maxJsonKeys: api.security?.maxJsonKeys,
758
+ audit: api.security?.audit
523
759
  }
524
760
  };
525
761
  }
526
762
  function validateSettings(settings) {
527
- const hasToken = Boolean(resolveToken(settings.security));
528
- if (settings.security.authentication !== 'none' && !hasToken) {
763
+ const credentials = resolveCredentials(settings.security);
764
+ if (settings.security.authentication !== 'none' && credentials.length === 0) {
529
765
  throw new Error('A mini API autenticada precisa de security.token ou NEUTRUN_API_TOKEN configurado.');
530
766
  }
767
+ if (settings.security.authentication === 'none' &&
768
+ isPublicHost(settings.host) &&
769
+ settings.security.allowInsecurePublic !== true) {
770
+ throw new Error('A mini API pública não pode desativar autenticação sem security.allowInsecurePublic=true.');
771
+ }
772
+ if (settings.security.cors === '*' &&
773
+ settings.security.authentication !== 'none') {
774
+ throw new Error('A mini API autenticada exige uma lista explícita de origens CORS.');
775
+ }
776
+ if (isPublicHost(settings.host) &&
777
+ credentials.some((credential) => Buffer.byteLength(credential.token, 'utf8') < 16)) {
778
+ throw new Error('Credenciais da mini API precisam ter pelo menos 16 bytes.');
779
+ }
531
780
  }
532
781
  function isPublicHost(host) {
533
782
  return !['127.0.0.1', 'localhost', '::1'].includes(host);
534
783
  }
535
- function resolveToken(security) {
536
- const token = security?.token?.trim();
537
- if (token)
538
- return token;
539
- return process.env[security?.tokenEnv ?? 'NEUTRUN_API_TOKEN']?.trim() || '';
784
+ function resolveCredentials(security) {
785
+ const values = [];
786
+ for (const credential of security?.credentials ?? [])
787
+ values.push(credential);
788
+ for (const token of security?.tokens ?? [])
789
+ values.push({ token });
790
+ const tokensEnvironment = process.env[security?.tokensEnv ?? 'NEUTRUN_API_TOKENS'];
791
+ for (const token of tokensEnvironment?.split(/[\r\n,]+/) ?? [])
792
+ values.push({ token });
793
+ const directToken = security?.token?.trim();
794
+ if (directToken)
795
+ values.push({ token: directToken });
796
+ const environmentToken = process.env[security?.tokenEnv ?? 'NEUTRUN_API_TOKEN']?.trim();
797
+ if (environmentToken)
798
+ values.push({ token: environmentToken });
799
+ const seen = new Set();
800
+ const output = [];
801
+ for (const value of values) {
802
+ const token = value.token?.trim();
803
+ if (!token || seen.has(token))
804
+ continue;
805
+ seen.add(token);
806
+ output.push({
807
+ token,
808
+ id: sanitizeCredentialId(value.id) ?? createCredentialId(token),
809
+ roles: sanitizePolicyValues(value.roles),
810
+ scopes: sanitizePolicyValues(value.scopes)
811
+ });
812
+ }
813
+ return output;
814
+ }
815
+ function anonymousIdentity() {
816
+ return { authentication: 'none', roles: [], scopes: [] };
817
+ }
818
+ function sanitizeCredentialId(value) {
819
+ const normalized = value?.trim();
820
+ if (!normalized)
821
+ return undefined;
822
+ return normalized.replace(/[^a-zA-Z0-9._:-]+/g, '_').slice(0, 100);
823
+ }
824
+ function createCredentialId(token) {
825
+ return `key_${(0, node_crypto_1.createHash)('sha256').update(token).digest('hex').slice(0, 12)}`;
826
+ }
827
+ function sanitizePolicyValues(values) {
828
+ return Array.from(new Set((values ?? [])
829
+ .map((value) => String(value).trim().slice(0, 100))
830
+ .filter(Boolean)))
831
+ .slice(0, 100);
832
+ }
833
+ function appendVaryHeader(response, value) {
834
+ const current = response.getHeader('Vary');
835
+ const entries = String(current ?? '')
836
+ .split(',')
837
+ .map((entry) => entry.trim())
838
+ .filter(Boolean);
839
+ if (!entries.some((entry) => entry.toLowerCase() === value.toLowerCase())) {
840
+ entries.push(value);
841
+ }
842
+ response.setHeader('Vary', entries.join(', '));
843
+ }
844
+ function setRateLimitHeaders(response, limit, remaining, resetMs) {
845
+ response.setHeader('RateLimit-Limit', limit);
846
+ response.setHeader('RateLimit-Remaining', remaining);
847
+ response.setHeader('RateLimit-Reset', Math.ceil(resetMs / 1_000));
848
+ }
849
+ function countObjectKeys(value, stopAfter) {
850
+ if (!value || typeof value !== 'object')
851
+ return 0;
852
+ let count = 0;
853
+ const stack = [value];
854
+ const seen = new WeakSet();
855
+ while (stack.length > 0 && count < stopAfter) {
856
+ const current = stack.pop();
857
+ if (seen.has(current))
858
+ continue;
859
+ seen.add(current);
860
+ for (const entry of Object.values(current)) {
861
+ count += 1;
862
+ if (entry && typeof entry === 'object')
863
+ stack.push(entry);
864
+ if (count >= stopAfter)
865
+ break;
866
+ }
867
+ }
868
+ return count;
869
+ }
870
+ function inspectObjectComplexity(value, maxDepth, maxKeys) {
871
+ const stack = [{ value, depth: 1 }];
872
+ const seen = new WeakSet();
873
+ let keys = 0;
874
+ while (stack.length > 0) {
875
+ const current = stack.pop();
876
+ if (seen.has(current.value))
877
+ continue;
878
+ seen.add(current.value);
879
+ if (current.depth > maxDepth)
880
+ return { valid: false, reason: 'json_depth' };
881
+ for (const entry of Object.values(current.value)) {
882
+ keys += 1;
883
+ if (keys > maxKeys)
884
+ return { valid: false, reason: 'json_keys' };
885
+ if (entry && typeof entry === 'object') {
886
+ stack.push({ value: entry, depth: current.depth + 1 });
887
+ }
888
+ }
889
+ }
890
+ return { valid: true };
891
+ }
892
+ function normalizeTelemetryPath(value) {
893
+ const raw = String(value || '/').split('?')[0] || '/';
894
+ return raw
895
+ .replace(/\b\d{6,}\b/g, ':id')
896
+ .replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi, ':id')
897
+ .slice(0, 120);
898
+ }
899
+ function clampInteger(value, minimum, maximum, fallback) {
900
+ const parsed = Number(value);
901
+ if (!Number.isFinite(parsed))
902
+ return fallback;
903
+ return Math.max(minimum, Math.min(maximum, Math.floor(parsed)));
540
904
  }
541
905
  function normalizePort(value) {
542
906
  const parsed = Number(value ?? DEFAULT_PORT);