@ninenity/neutrun 1.2.0 → 1.9.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.
@@ -0,0 +1,10 @@
1
+ export type BotEnvironment = 'production' | 'development';
2
+ /**
3
+ * Resolve o ambiente de execução do bot.
4
+ *
5
+ * Desenvolvimento só é ativado explicitamente. Isso evita que processos
6
+ * antigos, testes ou variáveis inválidas sejam classificados como DEV e
7
+ * contaminem métricas e logs de produção.
8
+ */
9
+ export declare const getBotEnvironment: (value?: unknown) => BotEnvironment;
10
+ //# sourceMappingURL=Environment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Environment.d.ts","sourceRoot":"","sources":["../../src/Shared/Environment.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,aAAa,CAAA;AAEzD;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,OAAO,KAAG,cAQnD,CAAA"}
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getBotEnvironment = void 0;
4
+ /**
5
+ * Resolve o ambiente de execução do bot.
6
+ *
7
+ * Desenvolvimento só é ativado explicitamente. Isso evita que processos
8
+ * antigos, testes ou variáveis inválidas sejam classificados como DEV e
9
+ * contaminem métricas e logs de produção.
10
+ */
11
+ const getBotEnvironment = (value) => {
12
+ const raw = String(value ?? process.env.BOT_ENV ?? process.env.API_ENV ?? process.env.NODE_ENV ?? '')
13
+ .trim()
14
+ .toLowerCase();
15
+ return raw === 'development' || raw === 'dev' ? 'development' : 'production';
16
+ };
17
+ exports.getBotEnvironment = getBotEnvironment;
18
+ //# sourceMappingURL=Environment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Environment.js","sourceRoot":"","sources":["../../src/Shared/Environment.ts"],"names":[],"mappings":";;;AAEA;;;;;;GAMG;AACI,MAAM,iBAAiB,GAAG,CAAC,KAAe,EAAkB,EAAE;IACnE,MAAM,GAAG,GAAG,MAAM,CAChB,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAClF;SACE,IAAI,EAAE;SACN,WAAW,EAAE,CAAA;IAEhB,OAAO,GAAG,KAAK,aAAa,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAA;AAC9E,CAAC,CAAA;AARY,QAAA,iBAAiB,qBAQ7B"}
@@ -1,19 +1,63 @@
1
- import { type Application } from 'express';
1
+ import { type Application, type Request, type RequestHandler, type Response as ExpressResponse } from 'express';
2
2
  export type ApiLifecycleMode = 'always' | 'idle' | 'suspend-resources';
3
3
  export interface ApiRateLimitOptions {
4
4
  windowMs?: number;
5
5
  max?: number;
6
6
  }
7
+ export interface ApiCredential {
8
+ token: string;
9
+ id?: string;
10
+ roles?: string[];
11
+ scopes?: string[];
12
+ }
13
+ export interface ApiIdentity {
14
+ authentication: 'none' | 'bearer' | 'api-key';
15
+ credentialId?: string;
16
+ roles: string[];
17
+ scopes: string[];
18
+ }
19
+ export interface ApiAuditEvent {
20
+ type: 'request' | 'authentication' | 'authorization' | 'rate-limit' | 'validation';
21
+ at: string;
22
+ requestId: string;
23
+ method: string;
24
+ path: string;
25
+ outcome: 'allowed' | 'denied' | 'success' | 'error';
26
+ status?: number;
27
+ credentialId?: string;
28
+ durationMs?: number;
29
+ reason?: string;
30
+ }
31
+ export interface ApiProtectionOptions {
32
+ roles?: string[];
33
+ scopes?: string[];
34
+ authorize?: (request: Request, identity: ApiIdentity) => boolean | Promise<boolean>;
35
+ }
36
+ export type ApiValidator = (value: unknown, request: Request) => boolean | string | void | Promise<boolean | string | void>;
37
+ export interface ApiValidationOptions {
38
+ body?: ApiValidator;
39
+ query?: ApiValidator;
40
+ params?: ApiValidator;
41
+ }
7
42
  export interface ApiSecurityOptions {
8
43
  authentication?: 'none' | 'bearer' | 'api-key';
9
44
  token?: string;
10
45
  tokenEnv?: string;
46
+ tokens?: string[];
47
+ tokensEnv?: string;
48
+ credentials?: ApiCredential[];
49
+ allowInsecurePublic?: boolean;
11
50
  publicPaths?: string[];
12
51
  cors?: false | '*' | string[];
13
52
  bodyLimit?: string | number;
14
53
  rateLimit?: false | ApiRateLimitOptions;
15
54
  requestTimeoutMs?: number;
16
55
  trustProxy?: boolean | number | string;
56
+ maxHeaderBytes?: number;
57
+ maxQueryParameters?: number;
58
+ maxJsonDepth?: number;
59
+ maxJsonKeys?: number;
60
+ audit?: (event: ApiAuditEvent) => Promise<void> | void;
17
61
  }
18
62
  export interface ApiLifecycleOptions {
19
63
  mode?: ApiLifecycleMode;
@@ -59,6 +103,9 @@ export interface ExpressApi extends Application {
59
103
  stop(): Promise<void>;
60
104
  fetch(path: string, options?: ApiRequestOptions): Promise<globalThis.Response>;
61
105
  fetchJson<T = unknown>(path: string, options?: ApiRequestOptions): Promise<ApiFetchResult<T>>;
106
+ protect(options?: ApiProtectionOptions): RequestHandler;
107
+ validate(options: ApiValidationOptions): RequestHandler;
108
+ getIdentity(response: ExpressResponse): ApiIdentity | undefined;
62
109
  readonly isRunning: boolean;
63
110
  readonly isSuspended: boolean;
64
111
  readonly url: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"ExpressApi.d.ts","sourceRoot":"","sources":["../../src/Shared/ExpressApi.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ExpressApi.d.ts","sourceRoot":"","sources":["../../src/Shared/ExpressApi.ts"],"names":[],"mappings":"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"}
@@ -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;
@@ -32,6 +33,11 @@ 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,9 @@ 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;
62
71
  Object.defineProperties(this.app, {
63
72
  isRunning: {
64
73
  enumerable: true,
@@ -131,7 +140,9 @@ class ExpressApiService {
131
140
  validateSettings(this.settings);
132
141
  this.finalize();
133
142
  this.startPromise = new Promise((resolve, reject) => {
134
- const server = (0, node_http_1.createServer)(this.app);
143
+ const server = (0, node_http_1.createServer)({
144
+ maxHeaderSize: clampInteger(this.settings.security.maxHeaderBytes, 8 * 1024, 64 * 1024, DEFAULT_MAX_HEADER_BYTES)
145
+ }, this.app);
135
146
  const requestTimeout = this.settings.security.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
136
147
  server.requestTimeout = requestTimeout;
137
148
  server.headersTimeout = Math.max(requestTimeout + 1_000, 5_000);
@@ -197,7 +208,7 @@ class ExpressApiService {
197
208
  await this.ensureStarted();
198
209
  const url = this.resolveUrl(path);
199
210
  const headers = new Headers(options.headers);
200
- const token = this.resolveToken();
211
+ const token = this.resolveCredentials()[0]?.token ?? '';
201
212
  const authentication = this.settings.security.authentication;
202
213
  if (token && authentication !== 'none' && !headers.has('authorization') && !headers.has('x-api-key')) {
203
214
  if (authentication === 'api-key')
@@ -205,6 +216,9 @@ class ExpressApiService {
205
216
  else
206
217
  headers.set('authorization', `Bearer ${token}`);
207
218
  }
219
+ if (!headers.has('x-request-id')) {
220
+ headers.set('x-request-id', Observability_1.default.getCorrelationId() ?? (0, node_crypto_1.randomUUID)());
221
+ }
208
222
  const body = serializeBody(options.body, headers);
209
223
  const { query, ...init } = options;
210
224
  const requestInit = {
@@ -225,9 +239,11 @@ class ExpressApiService {
225
239
  }
226
240
  installBuiltInMiddleware() {
227
241
  this.middlewareInstalled = true;
242
+ this.app.use(this.requestContextMiddleware());
228
243
  this.app.use(this.securityHeaders());
229
244
  this.app.use(this.corsMiddleware());
230
245
  this.app.use(this.requestTrackingMiddleware());
246
+ this.app.use(this.requestShapeLimitsMiddleware());
231
247
  this.app.use(this.requestTimeoutMiddleware());
232
248
  this.app.use(this.rateLimitMiddleware());
233
249
  this.app.use(this.authenticationMiddleware());
@@ -237,6 +253,16 @@ class ExpressApiService {
237
253
  extended: true,
238
254
  limit: this.settings.security.bodyLimit ?? '1mb'
239
255
  }));
256
+ this.app.use(this.jsonComplexityMiddleware());
257
+ }
258
+ requestContextMiddleware() {
259
+ return (req, res, next) => {
260
+ const incoming = req.get('x-request-id')?.trim() || '';
261
+ const requestId = REQUEST_ID_PATTERN.test(incoming) ? incoming : (0, node_crypto_1.randomUUID)();
262
+ res.locals.neutrunRequestId = requestId;
263
+ res.setHeader('X-Request-Id', requestId);
264
+ Observability_1.default.run({ correlationId: requestId, requestId, source: 'express-api' }, () => next());
265
+ };
240
266
  }
241
267
  securityHeaders() {
242
268
  return (_req, res, next) => {
@@ -244,6 +270,9 @@ class ExpressApiService {
244
270
  res.setHeader('X-Frame-Options', 'DENY');
245
271
  res.setHeader('Referrer-Policy', 'no-referrer');
246
272
  res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
273
+ res.setHeader('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'; base-uri 'none'");
274
+ res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()');
275
+ res.setHeader('Cache-Control', 'no-store');
247
276
  next();
248
277
  };
249
278
  }
@@ -260,8 +289,8 @@ class ExpressApiService {
260
289
  : undefined;
261
290
  if (allowed) {
262
291
  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');
292
+ appendVaryHeader(res, 'Origin');
293
+ res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, X-API-Key, X-Request-Id');
265
294
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
266
295
  }
267
296
  if (req.method === 'OPTIONS')
@@ -271,6 +300,7 @@ class ExpressApiService {
271
300
  }
272
301
  requestTrackingMiddleware() {
273
302
  return (req, res, next) => {
303
+ const startedAt = performance.now();
274
304
  this.lastActivityAt = Date.now();
275
305
  this.activeRequests += 1;
276
306
  let completed = false;
@@ -281,13 +311,49 @@ class ExpressApiService {
281
311
  this.activeRequests = Math.max(0, this.activeRequests - 1);
282
312
  this.lastActivityAt = Date.now();
283
313
  this.scheduleIdleCheck();
284
- this.recordRequest(req, res, res.statusCode);
314
+ this.recordRequest(req, res, res.statusCode, performance.now() - startedAt, !res.writableEnded);
285
315
  };
286
316
  res.once('finish', complete);
287
317
  res.once('close', complete);
288
318
  next();
289
319
  };
290
320
  }
321
+ requestShapeLimitsMiddleware() {
322
+ return (req, res, next) => {
323
+ const maximum = clampInteger(this.settings.security.maxQueryParameters, 1, 1_000, DEFAULT_MAX_QUERY_PARAMETERS);
324
+ if (countObjectKeys(req.query, maximum + 1) > maximum) {
325
+ this.emitAudit(req, res, {
326
+ type: 'validation',
327
+ outcome: 'denied',
328
+ status: 400,
329
+ reason: 'query_limit'
330
+ });
331
+ return res.status(400).json({ success: false, error: 'Too many query parameters' });
332
+ }
333
+ next();
334
+ };
335
+ }
336
+ jsonComplexityMiddleware() {
337
+ return (req, res, next) => {
338
+ if (req.body === undefined || req.body === null || typeof req.body !== 'object')
339
+ return next();
340
+ const maxDepth = clampInteger(this.settings.security.maxJsonDepth, 1, 100, DEFAULT_MAX_JSON_DEPTH);
341
+ const maxKeys = clampInteger(this.settings.security.maxJsonKeys, 1, 100_000, DEFAULT_MAX_JSON_KEYS);
342
+ const complexity = inspectObjectComplexity(req.body, maxDepth, maxKeys);
343
+ if (complexity.valid)
344
+ return next();
345
+ this.emitAudit(req, res, {
346
+ type: 'validation',
347
+ outcome: 'denied',
348
+ status: 400,
349
+ reason: complexity.reason
350
+ });
351
+ return res.status(400).json({
352
+ success: false,
353
+ error: 'Request body is too complex'
354
+ });
355
+ };
356
+ }
291
357
  requestTimeoutMiddleware() {
292
358
  return (_req, res, next) => {
293
359
  const timeoutMs = this.settings.security.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
@@ -320,11 +386,21 @@ class ExpressApiService {
320
386
  const current = this.rateLimitEntries.get(key);
321
387
  if (!current || now - current.startedAt >= windowMs) {
322
388
  this.rateLimitEntries.set(key, { startedAt: now, count: 1 });
389
+ setRateLimitHeaders(res, max, max - 1, windowMs);
323
390
  return next();
324
391
  }
325
- current.count += 1;
392
+ current.count = Math.min(Number.MAX_SAFE_INTEGER, current.count + 1);
393
+ const remaining = Math.max(0, max - current.count);
394
+ const resetMs = Math.max(0, windowMs - (now - current.startedAt));
395
+ setRateLimitHeaders(res, max, remaining, resetMs);
326
396
  if (current.count > max) {
327
- res.setHeader('Retry-After', Math.ceil((windowMs - (now - current.startedAt)) / 1_000));
397
+ res.setHeader('Retry-After', Math.ceil(resetMs / 1_000));
398
+ Observability_1.default.increment('neutrun_api_rate_limit_total');
399
+ this.emitAudit(req, res, {
400
+ type: 'rate-limit',
401
+ outcome: 'denied',
402
+ status: 429
403
+ });
328
404
  return res.status(429).json({
329
405
  success: false,
330
406
  error: 'Too Many Requests'
@@ -336,27 +412,118 @@ class ExpressApiService {
336
412
  authenticationMiddleware() {
337
413
  return (req, res, next) => {
338
414
  const authentication = this.settings.security.authentication;
339
- const token = this.resolveToken();
340
- if (authentication === 'none' || !token || isPublicPath(req.path, this.settings.security.publicPaths)) {
415
+ if (authentication === 'none') {
416
+ res.locals.neutrunIdentity = anonymousIdentity();
417
+ return next();
418
+ }
419
+ if (isPublicPath(req.path, this.settings.security.publicPaths)) {
420
+ res.locals.neutrunIdentity = anonymousIdentity();
341
421
  return next();
342
422
  }
343
423
  const received = authentication === 'api-key'
344
424
  ? req.get('x-api-key')
345
425
  : extractBearerToken(req.get('authorization'));
346
- if (!received || !timingSafeEqual(received, token)) {
426
+ const credential = received
427
+ ? this.resolveCredentials().find((entry) => timingSafeEqual(received, entry.token))
428
+ : undefined;
429
+ if (!credential) {
430
+ Observability_1.default.increment('neutrun_api_authentication_total', 1, { outcome: 'denied' });
431
+ this.emitAudit(req, res, {
432
+ type: 'authentication',
433
+ outcome: 'denied',
434
+ status: 401,
435
+ reason: received ? 'invalid_credential' : 'missing_credential'
436
+ });
347
437
  return res.status(401).json({
348
438
  success: false,
349
439
  error: 'Unauthorized'
350
440
  });
351
441
  }
442
+ const identity = {
443
+ authentication,
444
+ credentialId: credential.id,
445
+ roles: credential.roles,
446
+ scopes: credential.scopes
447
+ };
448
+ res.locals.neutrunIdentity = identity;
449
+ Observability_1.default.increment('neutrun_api_authentication_total', 1, { outcome: 'allowed' });
450
+ this.emitAudit(req, res, {
451
+ type: 'authentication',
452
+ outcome: 'allowed',
453
+ credentialId: credential.id
454
+ });
352
455
  next();
353
456
  };
354
457
  }
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() || '';
458
+ protectionMiddleware(options = {}) {
459
+ return (req, res, next) => {
460
+ const run = async () => {
461
+ const identity = res.locals.neutrunIdentity;
462
+ const roles = new Set(identity?.roles ?? []);
463
+ const scopes = new Set(identity?.scopes ?? []);
464
+ const hasRoles = (options.roles ?? []).every((role) => roles.has(role));
465
+ const hasScopes = (options.scopes ?? []).every((scope) => scopes.has(scope));
466
+ const policyEligible = Boolean(identity && identity.authentication !== 'none' && hasRoles && hasScopes);
467
+ const customAllowed = policyEligible && options.authorize
468
+ ? await options.authorize(req, identity)
469
+ : policyEligible;
470
+ if (policyEligible && identity && customAllowed) {
471
+ this.emitAudit(req, res, {
472
+ type: 'authorization',
473
+ outcome: 'allowed',
474
+ credentialId: identity.credentialId
475
+ });
476
+ next();
477
+ return;
478
+ }
479
+ Observability_1.default.increment('neutrun_api_authorization_total', 1, { outcome: 'denied' });
480
+ this.emitAudit(req, res, {
481
+ type: 'authorization',
482
+ outcome: 'denied',
483
+ status: identity?.authentication === 'none' ? 401 : 403,
484
+ credentialId: identity?.credentialId,
485
+ reason: 'policy_denied'
486
+ });
487
+ res.status(identity?.authentication === 'none' ? 401 : 403).json({
488
+ success: false,
489
+ error: identity?.authentication === 'none' ? 'Unauthorized' : 'Forbidden'
490
+ });
491
+ };
492
+ void run().catch(next);
493
+ };
494
+ }
495
+ validationMiddleware(options) {
496
+ return (req, res, next) => {
497
+ const run = async () => {
498
+ const checks = [
499
+ ['body', options.body, req.body],
500
+ ['query', options.query, req.query],
501
+ ['params', options.params, req.params]
502
+ ];
503
+ for (const [part, validator, value] of checks) {
504
+ if (!validator)
505
+ continue;
506
+ const result = await validator(value, req);
507
+ if (result === false || typeof result === 'string') {
508
+ const reason = typeof result === 'string' ? result.slice(0, 200) : `Invalid ${part}`;
509
+ Observability_1.default.increment('neutrun_api_validation_total', 1, { outcome: 'denied', part });
510
+ this.emitAudit(req, res, {
511
+ type: 'validation',
512
+ outcome: 'denied',
513
+ status: 400,
514
+ reason: part
515
+ });
516
+ res.status(400).json({ success: false, error: reason });
517
+ return;
518
+ }
519
+ }
520
+ next();
521
+ };
522
+ void run().catch(next);
523
+ };
524
+ }
525
+ resolveCredentials() {
526
+ return resolveCredentials(this.settings.security);
360
527
  }
361
528
  scheduleIdleCheck() {
362
529
  this.clearIdleTimer();
@@ -431,13 +598,42 @@ class ExpressApiService {
431
598
  this.cleanupRegistered = true;
432
599
  ProcessManager_1.default.registerCleanupTask(() => this.stop(), 'mini API Express');
433
600
  }
434
- recordRequest(req, res, status, error = false) {
435
- const path = req.path || req.originalUrl || '/';
601
+ recordRequest(req, res, status, durationMs, error = false) {
602
+ const path = normalizeTelemetryPath(req.route?.path ?? req.path ?? '/');
436
603
  TelemetryService_1.Telemetry.registerCounter('apiRequests');
437
604
  TelemetryService_1.Telemetry.registerEvent(`api:${req.method}:${path}`.slice(0, 120), {
438
- error: error || status >= 400
605
+ error: error || status >= 400,
606
+ durationMs
607
+ });
608
+ Observability_1.default.increment('neutrun_api_requests_total', 1, {
609
+ method: req.method,
610
+ outcome: status >= 400 || error ? 'error' : 'success',
611
+ status: `${Math.floor(status / 100)}xx`
612
+ });
613
+ Observability_1.default.observe('neutrun_api_request_duration_ms', durationMs, {
614
+ method: req.method
615
+ });
616
+ this.emitAudit(req, res, {
617
+ type: 'request',
618
+ outcome: status >= 400 || error ? 'error' : 'success',
619
+ status,
620
+ durationMs
621
+ });
622
+ }
623
+ emitAudit(req, res, event) {
624
+ const audit = this.settings.security.audit;
625
+ if (!audit)
626
+ return;
627
+ const payload = {
628
+ ...event,
629
+ at: new Date().toISOString(),
630
+ requestId: String(res.locals.neutrunRequestId || (0, node_crypto_1.randomUUID)()),
631
+ method: req.method.slice(0, 16),
632
+ path: normalizeTelemetryPath(req.route?.path ?? req.path ?? '/')
633
+ };
634
+ void Promise.resolve(audit(payload)).catch((error) => {
635
+ (0, ConsoleLog_1.logWarn)(`Falha no destino de auditoria da mini API: ${error instanceof Error ? error.message : error}`, 'Api');
439
636
  });
440
- void res;
441
637
  }
442
638
  resolveUrl(path) {
443
639
  const base = this.url;
@@ -511,32 +707,169 @@ function normalizeSettings(options) {
511
707
  onWake: lifecycle.onWake ?? serverlessOptions.onWake
512
708
  },
513
709
  security: {
514
- authentication: api.security?.authentication ?? (resolveToken(api.security) || isPublicHost(host) ? 'bearer' : 'none'),
710
+ authentication: api.security?.authentication ?? (resolveCredentials(api.security).length > 0 || isPublicHost(host) ? 'bearer' : 'none'),
515
711
  token: api.security?.token,
516
712
  tokenEnv: api.security?.tokenEnv ?? 'NEUTRUN_API_TOKEN',
713
+ tokens: api.security?.tokens,
714
+ tokensEnv: api.security?.tokensEnv ?? 'NEUTRUN_API_TOKENS',
715
+ credentials: api.security?.credentials,
716
+ allowInsecurePublic: api.security?.allowInsecurePublic,
517
717
  publicPaths: api.security?.publicPaths ?? [],
518
718
  cors: api.security?.cors,
519
719
  bodyLimit: api.security?.bodyLimit,
520
720
  rateLimit: api.security?.rateLimit,
521
721
  requestTimeoutMs: api.security?.requestTimeoutMs,
522
- trustProxy: api.security?.trustProxy
722
+ trustProxy: api.security?.trustProxy,
723
+ maxHeaderBytes: api.security?.maxHeaderBytes,
724
+ maxQueryParameters: api.security?.maxQueryParameters,
725
+ maxJsonDepth: api.security?.maxJsonDepth,
726
+ maxJsonKeys: api.security?.maxJsonKeys,
727
+ audit: api.security?.audit
523
728
  }
524
729
  };
525
730
  }
526
731
  function validateSettings(settings) {
527
- const hasToken = Boolean(resolveToken(settings.security));
528
- if (settings.security.authentication !== 'none' && !hasToken) {
732
+ const credentials = resolveCredentials(settings.security);
733
+ if (settings.security.authentication !== 'none' && credentials.length === 0) {
529
734
  throw new Error('A mini API autenticada precisa de security.token ou NEUTRUN_API_TOKEN configurado.');
530
735
  }
736
+ if (settings.security.authentication === 'none' &&
737
+ isPublicHost(settings.host) &&
738
+ settings.security.allowInsecurePublic !== true) {
739
+ throw new Error('A mini API pública não pode desativar autenticação sem security.allowInsecurePublic=true.');
740
+ }
741
+ if (settings.security.cors === '*' &&
742
+ settings.security.authentication !== 'none') {
743
+ throw new Error('A mini API autenticada exige uma lista explícita de origens CORS.');
744
+ }
745
+ if (isPublicHost(settings.host) &&
746
+ credentials.some((credential) => Buffer.byteLength(credential.token, 'utf8') < 16)) {
747
+ throw new Error('Credenciais da mini API precisam ter pelo menos 16 bytes.');
748
+ }
531
749
  }
532
750
  function isPublicHost(host) {
533
751
  return !['127.0.0.1', 'localhost', '::1'].includes(host);
534
752
  }
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() || '';
753
+ function resolveCredentials(security) {
754
+ const values = [];
755
+ for (const credential of security?.credentials ?? [])
756
+ values.push(credential);
757
+ for (const token of security?.tokens ?? [])
758
+ values.push({ token });
759
+ const tokensEnvironment = process.env[security?.tokensEnv ?? 'NEUTRUN_API_TOKENS'];
760
+ for (const token of tokensEnvironment?.split(/[\r\n,]+/) ?? [])
761
+ values.push({ token });
762
+ const directToken = security?.token?.trim();
763
+ if (directToken)
764
+ values.push({ token: directToken });
765
+ const environmentToken = process.env[security?.tokenEnv ?? 'NEUTRUN_API_TOKEN']?.trim();
766
+ if (environmentToken)
767
+ values.push({ token: environmentToken });
768
+ const seen = new Set();
769
+ const output = [];
770
+ for (const value of values) {
771
+ const token = value.token?.trim();
772
+ if (!token || seen.has(token))
773
+ continue;
774
+ seen.add(token);
775
+ output.push({
776
+ token,
777
+ id: sanitizeCredentialId(value.id) ?? createCredentialId(token),
778
+ roles: sanitizePolicyValues(value.roles),
779
+ scopes: sanitizePolicyValues(value.scopes)
780
+ });
781
+ }
782
+ return output;
783
+ }
784
+ function anonymousIdentity() {
785
+ return { authentication: 'none', roles: [], scopes: [] };
786
+ }
787
+ function sanitizeCredentialId(value) {
788
+ const normalized = value?.trim();
789
+ if (!normalized)
790
+ return undefined;
791
+ return normalized.replace(/[^a-zA-Z0-9._:-]+/g, '_').slice(0, 100);
792
+ }
793
+ function createCredentialId(token) {
794
+ return `key_${(0, node_crypto_1.createHash)('sha256').update(token).digest('hex').slice(0, 12)}`;
795
+ }
796
+ function sanitizePolicyValues(values) {
797
+ return Array.from(new Set((values ?? [])
798
+ .map((value) => String(value).trim().slice(0, 100))
799
+ .filter(Boolean)))
800
+ .slice(0, 100);
801
+ }
802
+ function appendVaryHeader(response, value) {
803
+ const current = response.getHeader('Vary');
804
+ const entries = String(current ?? '')
805
+ .split(',')
806
+ .map((entry) => entry.trim())
807
+ .filter(Boolean);
808
+ if (!entries.some((entry) => entry.toLowerCase() === value.toLowerCase())) {
809
+ entries.push(value);
810
+ }
811
+ response.setHeader('Vary', entries.join(', '));
812
+ }
813
+ function setRateLimitHeaders(response, limit, remaining, resetMs) {
814
+ response.setHeader('RateLimit-Limit', limit);
815
+ response.setHeader('RateLimit-Remaining', remaining);
816
+ response.setHeader('RateLimit-Reset', Math.ceil(resetMs / 1_000));
817
+ }
818
+ function countObjectKeys(value, stopAfter) {
819
+ if (!value || typeof value !== 'object')
820
+ return 0;
821
+ let count = 0;
822
+ const stack = [value];
823
+ const seen = new WeakSet();
824
+ while (stack.length > 0 && count < stopAfter) {
825
+ const current = stack.pop();
826
+ if (seen.has(current))
827
+ continue;
828
+ seen.add(current);
829
+ for (const entry of Object.values(current)) {
830
+ count += 1;
831
+ if (entry && typeof entry === 'object')
832
+ stack.push(entry);
833
+ if (count >= stopAfter)
834
+ break;
835
+ }
836
+ }
837
+ return count;
838
+ }
839
+ function inspectObjectComplexity(value, maxDepth, maxKeys) {
840
+ const stack = [{ value, depth: 1 }];
841
+ const seen = new WeakSet();
842
+ let keys = 0;
843
+ while (stack.length > 0) {
844
+ const current = stack.pop();
845
+ if (seen.has(current.value))
846
+ continue;
847
+ seen.add(current.value);
848
+ if (current.depth > maxDepth)
849
+ return { valid: false, reason: 'json_depth' };
850
+ for (const entry of Object.values(current.value)) {
851
+ keys += 1;
852
+ if (keys > maxKeys)
853
+ return { valid: false, reason: 'json_keys' };
854
+ if (entry && typeof entry === 'object') {
855
+ stack.push({ value: entry, depth: current.depth + 1 });
856
+ }
857
+ }
858
+ }
859
+ return { valid: true };
860
+ }
861
+ function normalizeTelemetryPath(value) {
862
+ const raw = String(value || '/').split('?')[0] || '/';
863
+ return raw
864
+ .replace(/\b\d{6,}\b/g, ':id')
865
+ .replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi, ':id')
866
+ .slice(0, 120);
867
+ }
868
+ function clampInteger(value, minimum, maximum, fallback) {
869
+ const parsed = Number(value);
870
+ if (!Number.isFinite(parsed))
871
+ return fallback;
872
+ return Math.max(minimum, Math.min(maximum, Math.floor(parsed)));
540
873
  }
541
874
  function normalizePort(value) {
542
875
  const parsed = Number(value ?? DEFAULT_PORT);