@ninenity/neutrun 1.0.0 → 1.2.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 +3 -2
- package/dist/Emojis/Pool.d.ts.map +1 -1
- package/dist/Emojis/Pool.js +3 -0
- package/dist/Emojis/Pool.js.map +1 -1
- package/dist/Logger/index.js +1 -1
- package/dist/Shared/ChannelLoader.d.ts.map +1 -1
- package/dist/Shared/ChannelLoader.js +22 -8
- package/dist/Shared/ChannelLoader.js.map +1 -1
- package/dist/Shared/Client.d.ts +4 -0
- package/dist/Shared/Client.d.ts.map +1 -1
- package/dist/Shared/Client.js +32 -43
- package/dist/Shared/Client.js.map +1 -1
- package/dist/Shared/ComponentRuntime.d.ts.map +1 -1
- package/dist/Shared/ComponentRuntime.js +14 -3
- package/dist/Shared/ComponentRuntime.js.map +1 -1
- package/dist/Shared/ConfigLoader.d.ts.map +1 -1
- package/dist/Shared/ConfigLoader.js +33 -8
- package/dist/Shared/ConfigLoader.js.map +1 -1
- package/dist/Shared/CooldownManager.d.ts.map +1 -1
- package/dist/Shared/CooldownManager.js +6 -0
- package/dist/Shared/CooldownManager.js.map +1 -1
- package/dist/Shared/Definer.d.ts.map +1 -1
- package/dist/Shared/Definer.js +0 -9
- package/dist/Shared/Definer.js.map +1 -1
- package/dist/Shared/EventBus.d.ts.map +1 -1
- package/dist/Shared/EventBus.js +4 -1
- package/dist/Shared/EventBus.js.map +1 -1
- package/dist/Shared/ExpressApi.d.ts +67 -0
- package/dist/Shared/ExpressApi.d.ts.map +1 -0
- package/dist/Shared/ExpressApi.js +676 -0
- package/dist/Shared/ExpressApi.js.map +1 -0
- package/dist/Shared/InteractionManager.d.ts.map +1 -1
- package/dist/Shared/InteractionManager.js +28 -4
- package/dist/Shared/InteractionManager.js.map +1 -1
- package/dist/Shared/LogTransport.d.ts +3 -1
- package/dist/Shared/LogTransport.d.ts.map +1 -1
- package/dist/Shared/LogTransport.js +19 -5
- package/dist/Shared/LogTransport.js.map +1 -1
- package/dist/Shared/Neutrun.d.ts +9 -9
- package/dist/Shared/Neutrun.d.ts.map +1 -1
- package/dist/Shared/Neutrun.js +87 -37
- package/dist/Shared/Neutrun.js.map +1 -1
- package/dist/Shared/NinenityApi.d.ts +50 -0
- package/dist/Shared/NinenityApi.d.ts.map +1 -0
- package/dist/Shared/NinenityApi.js +206 -0
- package/dist/Shared/NinenityApi.js.map +1 -0
- package/dist/Shared/Plugin.d.ts +1 -1
- package/dist/Shared/Plugin.d.ts.map +1 -1
- package/dist/Shared/Pool.d.ts.map +1 -1
- package/dist/Shared/Pool.js +3 -0
- package/dist/Shared/Pool.js.map +1 -1
- package/dist/Shared/ProcessManager.d.ts +2 -1
- package/dist/Shared/ProcessManager.d.ts.map +1 -1
- package/dist/Shared/ProcessManager.js +56 -26
- package/dist/Shared/ProcessManager.js.map +1 -1
- package/dist/Shared/QueueManager.d.ts.map +1 -1
- package/dist/Shared/QueueManager.js +35 -13
- package/dist/Shared/QueueManager.js.map +1 -1
- package/dist/Shared/RoleLoader.d.ts.map +1 -1
- package/dist/Shared/RoleLoader.js +22 -8
- package/dist/Shared/RoleLoader.js.map +1 -1
- package/dist/Shared/RuntimeAliases.d.ts +10 -0
- package/dist/Shared/RuntimeAliases.d.ts.map +1 -0
- package/dist/Shared/RuntimeAliases.js +29 -0
- package/dist/Shared/RuntimeAliases.js.map +1 -0
- package/dist/Shared/Supervisor.d.ts.map +1 -1
- package/dist/Shared/Supervisor.js +59 -6
- package/dist/Shared/Supervisor.js.map +1 -1
- package/dist/Shared/TelemetryService.js +1 -1
- package/dist/Shared/TelemetryService.js.map +1 -1
- package/dist/Shared/WebSocket.d.ts +1 -1
- package/dist/Shared/WebSocket.d.ts.map +1 -1
- package/dist/Shared/WebSocket.js +59 -32
- package/dist/Shared/WebSocket.js.map +1 -1
- package/dist/Types/Client.Type.d.ts +3 -3
- package/dist/Types/Client.Type.d.ts.map +1 -1
- package/dist/Types/Client.Type.js.map +1 -1
- package/dist/api.d.ts +3 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +8 -0
- package/dist/api.js.map +1 -0
- package/dist/discordjs.d.ts +8 -0
- package/dist/discordjs.d.ts.map +1 -0
- package/dist/discordjs.js +24 -0
- package/dist/discordjs.js.map +1 -0
- package/dist/express.d.ts +11 -0
- package/dist/express.d.ts.map +1 -0
- package/dist/express.js +16 -0
- package/dist/express.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -2
- package/dist/index.js.map +1 -1
- package/package.json +22 -5
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Api = exports.ApiRequestError = void 0;
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const node_http_1 = require("node:http");
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const ConsoleLog_1 = require("./ConsoleLog");
|
|
11
|
+
const ProcessManager_1 = __importDefault(require("./ProcessManager"));
|
|
12
|
+
const TelemetryService_1 = require("./TelemetryService");
|
|
13
|
+
class ApiRequestError extends Error {
|
|
14
|
+
status;
|
|
15
|
+
data;
|
|
16
|
+
response;
|
|
17
|
+
constructor(message, response, data) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'ApiRequestError';
|
|
20
|
+
this.status = response.status;
|
|
21
|
+
this.response = response;
|
|
22
|
+
this.data = data;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.ApiRequestError = ApiRequestError;
|
|
26
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
27
|
+
const DEFAULT_PORT = 3000;
|
|
28
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
29
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30 * 1000;
|
|
30
|
+
const DEFAULT_RATE_LIMIT_WINDOW_MS = 60 * 1000;
|
|
31
|
+
const DEFAULT_RATE_LIMIT_MAX = 60;
|
|
32
|
+
const MAX_RATE_LIMIT_ENTRIES = 10_000;
|
|
33
|
+
const SERVER_CLOSE_TIMEOUT_MS = 5_000;
|
|
34
|
+
const MAX_INTERNAL_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
35
|
+
class ExpressApiService {
|
|
36
|
+
app;
|
|
37
|
+
settings = normalizeSettings(undefined);
|
|
38
|
+
configured = false;
|
|
39
|
+
finalized = false;
|
|
40
|
+
server = null;
|
|
41
|
+
startPromise = null;
|
|
42
|
+
stopPromise = null;
|
|
43
|
+
idleTimer = null;
|
|
44
|
+
activeRequests = 0;
|
|
45
|
+
lastActivityAt = Date.now();
|
|
46
|
+
resourcesSuspended = false;
|
|
47
|
+
cleanupRegistered = false;
|
|
48
|
+
middlewareInstalled = false;
|
|
49
|
+
rateLimitEntries = new Map();
|
|
50
|
+
rateLimitLastSweepAt = 0;
|
|
51
|
+
resourceTransition = null;
|
|
52
|
+
constructor() {
|
|
53
|
+
this.app = (0, express_1.default)();
|
|
54
|
+
this.app.disable('x-powered-by');
|
|
55
|
+
this.app.configureNeutrun = (options) => this.configure(options);
|
|
56
|
+
this.app.finalize = () => this.finalize();
|
|
57
|
+
this.app.start = (options) => this.start(options);
|
|
58
|
+
this.app.ensureStarted = () => this.ensureStarted();
|
|
59
|
+
this.app.stop = () => this.stop();
|
|
60
|
+
this.app.fetch = (path, options) => this.fetch(path, options);
|
|
61
|
+
this.app.fetchJson = (path, options) => this.fetchJson(path, options);
|
|
62
|
+
Object.defineProperties(this.app, {
|
|
63
|
+
isRunning: {
|
|
64
|
+
enumerable: true,
|
|
65
|
+
get: () => this.server !== null
|
|
66
|
+
},
|
|
67
|
+
isSuspended: {
|
|
68
|
+
enumerable: true,
|
|
69
|
+
get: () => this.resourcesSuspended
|
|
70
|
+
},
|
|
71
|
+
url: {
|
|
72
|
+
enumerable: true,
|
|
73
|
+
get: () => this.url
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
configure(options) {
|
|
78
|
+
if (this.server) {
|
|
79
|
+
throw new Error('Api não pode ser reconfigurada enquanto o servidor está ativo.');
|
|
80
|
+
}
|
|
81
|
+
const nextSettings = normalizeSettings(options);
|
|
82
|
+
if (this.middlewareInstalled &&
|
|
83
|
+
String(nextSettings.security.bodyLimit ?? '1mb') !==
|
|
84
|
+
String(this.settings.security.bodyLimit ?? '1mb')) {
|
|
85
|
+
throw new Error('security.bodyLimit não pode mudar depois que os middlewares da Api foram instalados.');
|
|
86
|
+
}
|
|
87
|
+
this.settings = nextSettings;
|
|
88
|
+
this.configured = options !== false && options !== undefined;
|
|
89
|
+
this.app.set('trust proxy', this.settings.security.trustProxy ?? false);
|
|
90
|
+
this.app.disable('x-powered-by');
|
|
91
|
+
if (!this.configured || !this.settings.enabled)
|
|
92
|
+
return;
|
|
93
|
+
validateSettings(this.settings);
|
|
94
|
+
if (this.middlewareInstalled)
|
|
95
|
+
return;
|
|
96
|
+
this.installBuiltInMiddleware();
|
|
97
|
+
}
|
|
98
|
+
finalize() {
|
|
99
|
+
if (this.finalized)
|
|
100
|
+
return;
|
|
101
|
+
this.finalized = true;
|
|
102
|
+
this.app.use((_req, res) => {
|
|
103
|
+
res.status(404).json({
|
|
104
|
+
success: false,
|
|
105
|
+
error: 'Not Found'
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
this.app.use((error, _req, res, next) => {
|
|
109
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110
|
+
const status = isHttpStatus(error) ? error.status : 500;
|
|
111
|
+
(0, ConsoleLog_1.logError)(`Erro na mini API: ${message}`, 'Api');
|
|
112
|
+
if (res.headersSent)
|
|
113
|
+
return next(error);
|
|
114
|
+
res.status(status).json({
|
|
115
|
+
success: false,
|
|
116
|
+
error: status >= 500 ? 'Internal Server Error' : message
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async start(options) {
|
|
121
|
+
if (this.stopPromise)
|
|
122
|
+
await this.stopPromise;
|
|
123
|
+
if (options !== undefined)
|
|
124
|
+
this.configure(options);
|
|
125
|
+
if (!this.configured || !this.settings.enabled)
|
|
126
|
+
return;
|
|
127
|
+
if (this.server)
|
|
128
|
+
return;
|
|
129
|
+
if (this.startPromise)
|
|
130
|
+
return this.startPromise;
|
|
131
|
+
validateSettings(this.settings);
|
|
132
|
+
this.finalize();
|
|
133
|
+
this.startPromise = new Promise((resolve, reject) => {
|
|
134
|
+
const server = (0, node_http_1.createServer)(this.app);
|
|
135
|
+
const requestTimeout = this.settings.security.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
136
|
+
server.requestTimeout = requestTimeout;
|
|
137
|
+
server.headersTimeout = Math.max(requestTimeout + 1_000, 5_000);
|
|
138
|
+
server.keepAliveTimeout = Math.min(requestTimeout, 5_000);
|
|
139
|
+
const onError = (error) => {
|
|
140
|
+
server.removeListener('listening', onListening);
|
|
141
|
+
try {
|
|
142
|
+
server.close();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// O servidor ainda pode não ter aberto um handle.
|
|
146
|
+
}
|
|
147
|
+
reject(error);
|
|
148
|
+
};
|
|
149
|
+
const onListening = () => {
|
|
150
|
+
server.removeListener('error', onError);
|
|
151
|
+
this.server = server;
|
|
152
|
+
this.startPromise = null;
|
|
153
|
+
this.lastActivityAt = Date.now();
|
|
154
|
+
this.registerCleanup();
|
|
155
|
+
this.scheduleIdleCheck();
|
|
156
|
+
const address = server.address();
|
|
157
|
+
const actualPort = typeof address === 'object' && address ? address.port : this.settings.port;
|
|
158
|
+
(0, ConsoleLog_1.logSuccess)(`Mini API Express ativa em ${this.publicUrl(actualPort)}`, 'Api');
|
|
159
|
+
resolve();
|
|
160
|
+
};
|
|
161
|
+
server.once('error', onError);
|
|
162
|
+
server.once('listening', onListening);
|
|
163
|
+
server.listen(this.settings.port, this.settings.host);
|
|
164
|
+
}).catch((error) => {
|
|
165
|
+
this.startPromise = null;
|
|
166
|
+
(0, ConsoleLog_1.logError)(`Falha ao iniciar a mini API: ${error instanceof Error ? error.message : error}`, 'Api');
|
|
167
|
+
throw error;
|
|
168
|
+
});
|
|
169
|
+
return this.startPromise;
|
|
170
|
+
}
|
|
171
|
+
async ensureStarted() {
|
|
172
|
+
if (!this.configured || !this.settings.enabled) {
|
|
173
|
+
throw new Error('A mini API está desativada. Configure settings.api no Neutrun.init().');
|
|
174
|
+
}
|
|
175
|
+
if (!this.server)
|
|
176
|
+
await this.start();
|
|
177
|
+
await this.wakeResources();
|
|
178
|
+
}
|
|
179
|
+
async stop() {
|
|
180
|
+
if (this.stopPromise)
|
|
181
|
+
return this.stopPromise;
|
|
182
|
+
this.clearIdleTimer();
|
|
183
|
+
if (this.startPromise) {
|
|
184
|
+
await this.startPromise.catch(() => undefined);
|
|
185
|
+
}
|
|
186
|
+
const server = this.server;
|
|
187
|
+
if (!server)
|
|
188
|
+
return;
|
|
189
|
+
this.server = null;
|
|
190
|
+
this.stopPromise = closeHttpServer(server).finally(() => {
|
|
191
|
+
this.stopPromise = null;
|
|
192
|
+
});
|
|
193
|
+
await this.stopPromise;
|
|
194
|
+
(0, ConsoleLog_1.logInfo)('Mini API Express encerrada.', 'Api');
|
|
195
|
+
}
|
|
196
|
+
async fetch(path, options = {}) {
|
|
197
|
+
await this.ensureStarted();
|
|
198
|
+
const url = this.resolveUrl(path);
|
|
199
|
+
const headers = new Headers(options.headers);
|
|
200
|
+
const token = this.resolveToken();
|
|
201
|
+
const authentication = this.settings.security.authentication;
|
|
202
|
+
if (token && authentication !== 'none' && !headers.has('authorization') && !headers.has('x-api-key')) {
|
|
203
|
+
if (authentication === 'api-key')
|
|
204
|
+
headers.set('x-api-key', token);
|
|
205
|
+
else
|
|
206
|
+
headers.set('authorization', `Bearer ${token}`);
|
|
207
|
+
}
|
|
208
|
+
const body = serializeBody(options.body, headers);
|
|
209
|
+
const { query, ...init } = options;
|
|
210
|
+
const requestInit = {
|
|
211
|
+
...init,
|
|
212
|
+
method: options.method ?? (body === undefined ? 'GET' : 'POST'),
|
|
213
|
+
headers,
|
|
214
|
+
body
|
|
215
|
+
};
|
|
216
|
+
return globalThis.fetch(addQuery(url, query), requestInit);
|
|
217
|
+
}
|
|
218
|
+
async fetchJson(path, options = {}) {
|
|
219
|
+
const response = await this.fetch(path, options);
|
|
220
|
+
const data = await parseResponse(response);
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
throw new ApiRequestError(`Mini API request failed with HTTP ${response.status}.`, response, data);
|
|
223
|
+
}
|
|
224
|
+
return { data, response };
|
|
225
|
+
}
|
|
226
|
+
installBuiltInMiddleware() {
|
|
227
|
+
this.middlewareInstalled = true;
|
|
228
|
+
this.app.use(this.securityHeaders());
|
|
229
|
+
this.app.use(this.corsMiddleware());
|
|
230
|
+
this.app.use(this.requestTrackingMiddleware());
|
|
231
|
+
this.app.use(this.requestTimeoutMiddleware());
|
|
232
|
+
this.app.use(this.rateLimitMiddleware());
|
|
233
|
+
this.app.use(this.authenticationMiddleware());
|
|
234
|
+
this.app.use(this.wakeResourcesMiddleware());
|
|
235
|
+
this.app.use(express_1.default.json({ limit: this.settings.security.bodyLimit ?? '1mb' }));
|
|
236
|
+
this.app.use(express_1.default.urlencoded({
|
|
237
|
+
extended: true,
|
|
238
|
+
limit: this.settings.security.bodyLimit ?? '1mb'
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
securityHeaders() {
|
|
242
|
+
return (_req, res, next) => {
|
|
243
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
244
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
245
|
+
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
246
|
+
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
|
|
247
|
+
next();
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
corsMiddleware() {
|
|
251
|
+
return (req, res, next) => {
|
|
252
|
+
const cors = this.settings.security.cors;
|
|
253
|
+
if (cors === false || cors === undefined)
|
|
254
|
+
return next();
|
|
255
|
+
const origin = req.headers.origin;
|
|
256
|
+
const allowed = cors === '*'
|
|
257
|
+
? '*'
|
|
258
|
+
: origin && cors.includes(origin)
|
|
259
|
+
? origin
|
|
260
|
+
: undefined;
|
|
261
|
+
if (allowed) {
|
|
262
|
+
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');
|
|
265
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
266
|
+
}
|
|
267
|
+
if (req.method === 'OPTIONS')
|
|
268
|
+
return res.sendStatus(allowed ? 204 : 403);
|
|
269
|
+
next();
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
requestTrackingMiddleware() {
|
|
273
|
+
return (req, res, next) => {
|
|
274
|
+
this.lastActivityAt = Date.now();
|
|
275
|
+
this.activeRequests += 1;
|
|
276
|
+
let completed = false;
|
|
277
|
+
const complete = () => {
|
|
278
|
+
if (completed)
|
|
279
|
+
return;
|
|
280
|
+
completed = true;
|
|
281
|
+
this.activeRequests = Math.max(0, this.activeRequests - 1);
|
|
282
|
+
this.lastActivityAt = Date.now();
|
|
283
|
+
this.scheduleIdleCheck();
|
|
284
|
+
this.recordRequest(req, res, res.statusCode);
|
|
285
|
+
};
|
|
286
|
+
res.once('finish', complete);
|
|
287
|
+
res.once('close', complete);
|
|
288
|
+
next();
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
requestTimeoutMiddleware() {
|
|
292
|
+
return (_req, res, next) => {
|
|
293
|
+
const timeoutMs = this.settings.security.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
294
|
+
res.setTimeout(timeoutMs, () => {
|
|
295
|
+
if (!res.headersSent) {
|
|
296
|
+
res.status(504).json({ success: false, error: 'Gateway Timeout' });
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
res.destroy();
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
next();
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
wakeResourcesMiddleware() {
|
|
306
|
+
return (_req, _res, next) => {
|
|
307
|
+
void this.wakeResources().then(() => next()).catch(next);
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
rateLimitMiddleware() {
|
|
311
|
+
return (req, res, next) => {
|
|
312
|
+
const options = this.settings.security.rateLimit;
|
|
313
|
+
if (options === false || options === undefined)
|
|
314
|
+
return next();
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
const key = req.ip || req.socket.remoteAddress || 'unknown';
|
|
317
|
+
const windowMs = Math.max(1_000, options.windowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS);
|
|
318
|
+
const max = Math.max(1, options.max ?? DEFAULT_RATE_LIMIT_MAX);
|
|
319
|
+
this.pruneRateLimits(now, windowMs);
|
|
320
|
+
const current = this.rateLimitEntries.get(key);
|
|
321
|
+
if (!current || now - current.startedAt >= windowMs) {
|
|
322
|
+
this.rateLimitEntries.set(key, { startedAt: now, count: 1 });
|
|
323
|
+
return next();
|
|
324
|
+
}
|
|
325
|
+
current.count += 1;
|
|
326
|
+
if (current.count > max) {
|
|
327
|
+
res.setHeader('Retry-After', Math.ceil((windowMs - (now - current.startedAt)) / 1_000));
|
|
328
|
+
return res.status(429).json({
|
|
329
|
+
success: false,
|
|
330
|
+
error: 'Too Many Requests'
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
next();
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
authenticationMiddleware() {
|
|
337
|
+
return (req, res, next) => {
|
|
338
|
+
const authentication = this.settings.security.authentication;
|
|
339
|
+
const token = this.resolveToken();
|
|
340
|
+
if (authentication === 'none' || !token || isPublicPath(req.path, this.settings.security.publicPaths)) {
|
|
341
|
+
return next();
|
|
342
|
+
}
|
|
343
|
+
const received = authentication === 'api-key'
|
|
344
|
+
? req.get('x-api-key')
|
|
345
|
+
: extractBearerToken(req.get('authorization'));
|
|
346
|
+
if (!received || !timingSafeEqual(received, token)) {
|
|
347
|
+
return res.status(401).json({
|
|
348
|
+
success: false,
|
|
349
|
+
error: 'Unauthorized'
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
next();
|
|
353
|
+
};
|
|
354
|
+
}
|
|
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() || '';
|
|
360
|
+
}
|
|
361
|
+
scheduleIdleCheck() {
|
|
362
|
+
this.clearIdleTimer();
|
|
363
|
+
const lifecycle = this.settings.lifecycle;
|
|
364
|
+
if (!this.server || lifecycle.mode === 'always')
|
|
365
|
+
return;
|
|
366
|
+
const idleTimeoutMs = lifecycle.idleTimeoutMs;
|
|
367
|
+
this.idleTimer = setTimeout(() => {
|
|
368
|
+
void this.handleIdle().catch((error) => {
|
|
369
|
+
(0, ConsoleLog_1.logWarn)(`Falha no ciclo de ociosidade da mini API: ${error instanceof Error ? error.message : error}`, 'Api');
|
|
370
|
+
this.lastActivityAt = Date.now();
|
|
371
|
+
this.scheduleIdleCheck();
|
|
372
|
+
});
|
|
373
|
+
}, Math.max(1_000, idleTimeoutMs - (Date.now() - this.lastActivityAt)));
|
|
374
|
+
this.idleTimer.unref?.();
|
|
375
|
+
}
|
|
376
|
+
async handleIdle() {
|
|
377
|
+
if (!this.server || this.activeRequests > 0) {
|
|
378
|
+
this.scheduleIdleCheck();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (Date.now() - this.lastActivityAt < this.settings.lifecycle.idleTimeoutMs) {
|
|
382
|
+
this.scheduleIdleCheck();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
await this.suspendResources();
|
|
386
|
+
if (this.settings.lifecycle.mode === 'idle') {
|
|
387
|
+
await this.stop();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async suspendResources() {
|
|
391
|
+
if (this.resourceTransition)
|
|
392
|
+
await this.resourceTransition;
|
|
393
|
+
if (this.resourcesSuspended)
|
|
394
|
+
return;
|
|
395
|
+
const transition = (async () => {
|
|
396
|
+
await this.settings.lifecycle.onIdle?.();
|
|
397
|
+
this.resourcesSuspended = true;
|
|
398
|
+
TelemetryService_1.Telemetry.registerEvent('api:idle');
|
|
399
|
+
})();
|
|
400
|
+
this.resourceTransition = transition;
|
|
401
|
+
try {
|
|
402
|
+
await transition;
|
|
403
|
+
}
|
|
404
|
+
finally {
|
|
405
|
+
if (this.resourceTransition === transition)
|
|
406
|
+
this.resourceTransition = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async wakeResources() {
|
|
410
|
+
if (this.resourceTransition)
|
|
411
|
+
await this.resourceTransition;
|
|
412
|
+
if (!this.resourcesSuspended)
|
|
413
|
+
return;
|
|
414
|
+
const transition = (async () => {
|
|
415
|
+
await this.settings.lifecycle.onWake?.();
|
|
416
|
+
this.resourcesSuspended = false;
|
|
417
|
+
TelemetryService_1.Telemetry.registerEvent('api:wake');
|
|
418
|
+
})();
|
|
419
|
+
this.resourceTransition = transition;
|
|
420
|
+
try {
|
|
421
|
+
await transition;
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
if (this.resourceTransition === transition)
|
|
425
|
+
this.resourceTransition = null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
registerCleanup() {
|
|
429
|
+
if (this.cleanupRegistered)
|
|
430
|
+
return;
|
|
431
|
+
this.cleanupRegistered = true;
|
|
432
|
+
ProcessManager_1.default.registerCleanupTask(() => this.stop(), 'mini API Express');
|
|
433
|
+
}
|
|
434
|
+
recordRequest(req, res, status, error = false) {
|
|
435
|
+
const path = req.path || req.originalUrl || '/';
|
|
436
|
+
TelemetryService_1.Telemetry.registerCounter('apiRequests');
|
|
437
|
+
TelemetryService_1.Telemetry.registerEvent(`api:${req.method}:${path}`.slice(0, 120), {
|
|
438
|
+
error: error || status >= 400
|
|
439
|
+
});
|
|
440
|
+
void res;
|
|
441
|
+
}
|
|
442
|
+
resolveUrl(path) {
|
|
443
|
+
const base = this.url;
|
|
444
|
+
if (!base)
|
|
445
|
+
throw new Error('A mini API ainda não está ativa.');
|
|
446
|
+
const resolved = /^https?:\/\//i.test(path)
|
|
447
|
+
? new URL(path)
|
|
448
|
+
: new URL(path.replace(/^\/+/, ''), `${base}/`);
|
|
449
|
+
if (resolved.origin !== new URL(base).origin) {
|
|
450
|
+
throw new Error('Api.fetch aceita apenas URLs da própria mini API.');
|
|
451
|
+
}
|
|
452
|
+
return resolved.toString();
|
|
453
|
+
}
|
|
454
|
+
publicUrl(port) {
|
|
455
|
+
const host = this.settings.host === '0.0.0.0' || this.settings.host === '::'
|
|
456
|
+
? '127.0.0.1'
|
|
457
|
+
: this.settings.host;
|
|
458
|
+
return `http://${formatHost(host)}:${port}`;
|
|
459
|
+
}
|
|
460
|
+
get url() {
|
|
461
|
+
if (!this.server)
|
|
462
|
+
return null;
|
|
463
|
+
const address = this.server.address();
|
|
464
|
+
const port = typeof address === 'object' && address ? address.port : this.settings.port;
|
|
465
|
+
return this.publicUrl(port);
|
|
466
|
+
}
|
|
467
|
+
clearIdleTimer() {
|
|
468
|
+
if (!this.idleTimer)
|
|
469
|
+
return;
|
|
470
|
+
clearTimeout(this.idleTimer);
|
|
471
|
+
this.idleTimer = null;
|
|
472
|
+
}
|
|
473
|
+
pruneRateLimits(now, windowMs) {
|
|
474
|
+
if (this.rateLimitEntries.size < MAX_RATE_LIMIT_ENTRIES &&
|
|
475
|
+
now - this.rateLimitLastSweepAt < windowMs)
|
|
476
|
+
return;
|
|
477
|
+
this.rateLimitLastSweepAt = now;
|
|
478
|
+
for (const [key, entry] of this.rateLimitEntries) {
|
|
479
|
+
if (now - entry.startedAt >= windowMs)
|
|
480
|
+
this.rateLimitEntries.delete(key);
|
|
481
|
+
}
|
|
482
|
+
while (this.rateLimitEntries.size >= MAX_RATE_LIMIT_ENTRIES) {
|
|
483
|
+
const oldest = this.rateLimitEntries.keys().next().value;
|
|
484
|
+
if (!oldest)
|
|
485
|
+
break;
|
|
486
|
+
this.rateLimitEntries.delete(oldest);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const service = new ExpressApiService();
|
|
491
|
+
exports.Api = service.app;
|
|
492
|
+
function normalizeSettings(options) {
|
|
493
|
+
const api = options === false ? {} : options ?? {};
|
|
494
|
+
const host = api.host?.trim() || process.env.NEUTRUN_API_HOST?.trim() || DEFAULT_HOST;
|
|
495
|
+
const serverless = api.serverless;
|
|
496
|
+
const lifecycle = api.lifecycle ?? {};
|
|
497
|
+
const serverlessOptions = typeof serverless === 'object' ? serverless : {};
|
|
498
|
+
const serverlessEnabled = serverless === true || serverlessOptions.enabled === true;
|
|
499
|
+
const lifecycleMode = serverlessEnabled
|
|
500
|
+
? lifecycle.mode ?? 'idle'
|
|
501
|
+
: lifecycle.mode ?? 'always';
|
|
502
|
+
const idleTimeoutMs = parseDuration(lifecycle.idleTimeoutMs ?? serverlessOptions.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS);
|
|
503
|
+
return {
|
|
504
|
+
enabled: api.enabled ?? (options !== undefined && options !== false),
|
|
505
|
+
host,
|
|
506
|
+
port: normalizePort(api.port ?? process.env.NEUTRUN_API_PORT),
|
|
507
|
+
lifecycle: {
|
|
508
|
+
mode: lifecycleMode,
|
|
509
|
+
idleTimeoutMs,
|
|
510
|
+
onIdle: lifecycle.onIdle ?? serverlessOptions.onIdle,
|
|
511
|
+
onWake: lifecycle.onWake ?? serverlessOptions.onWake
|
|
512
|
+
},
|
|
513
|
+
security: {
|
|
514
|
+
authentication: api.security?.authentication ?? (resolveToken(api.security) || isPublicHost(host) ? 'bearer' : 'none'),
|
|
515
|
+
token: api.security?.token,
|
|
516
|
+
tokenEnv: api.security?.tokenEnv ?? 'NEUTRUN_API_TOKEN',
|
|
517
|
+
publicPaths: api.security?.publicPaths ?? [],
|
|
518
|
+
cors: api.security?.cors,
|
|
519
|
+
bodyLimit: api.security?.bodyLimit,
|
|
520
|
+
rateLimit: api.security?.rateLimit,
|
|
521
|
+
requestTimeoutMs: api.security?.requestTimeoutMs,
|
|
522
|
+
trustProxy: api.security?.trustProxy
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function validateSettings(settings) {
|
|
527
|
+
const hasToken = Boolean(resolveToken(settings.security));
|
|
528
|
+
if (settings.security.authentication !== 'none' && !hasToken) {
|
|
529
|
+
throw new Error('A mini API autenticada precisa de security.token ou NEUTRUN_API_TOKEN configurado.');
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function isPublicHost(host) {
|
|
533
|
+
return !['127.0.0.1', 'localhost', '::1'].includes(host);
|
|
534
|
+
}
|
|
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() || '';
|
|
540
|
+
}
|
|
541
|
+
function normalizePort(value) {
|
|
542
|
+
const parsed = Number(value ?? DEFAULT_PORT);
|
|
543
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65_535) {
|
|
544
|
+
throw new TypeError('A porta da mini API deve ser um inteiro entre 0 e 65535.');
|
|
545
|
+
}
|
|
546
|
+
return parsed;
|
|
547
|
+
}
|
|
548
|
+
function parseDuration(value) {
|
|
549
|
+
if (typeof value === 'number')
|
|
550
|
+
return Math.max(1_000, value);
|
|
551
|
+
const normalized = value.trim().toLowerCase();
|
|
552
|
+
const match = normalized.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h)?$/);
|
|
553
|
+
if (!match)
|
|
554
|
+
throw new TypeError(`Duração inválida para a mini API: ${value}`);
|
|
555
|
+
const amount = Number(match[1]);
|
|
556
|
+
const multiplier = match[2] === 'h'
|
|
557
|
+
? 60 * 60 * 1_000
|
|
558
|
+
: match[2] === 'm'
|
|
559
|
+
? 60 * 1_000
|
|
560
|
+
: match[2] === 's'
|
|
561
|
+
? 1_000
|
|
562
|
+
: 1;
|
|
563
|
+
return Math.max(1_000, Math.round(amount * multiplier));
|
|
564
|
+
}
|
|
565
|
+
function isPublicPath(path, paths) {
|
|
566
|
+
return (paths ?? []).some((entry) => {
|
|
567
|
+
const normalized = entry.trim();
|
|
568
|
+
if (!normalized)
|
|
569
|
+
return false;
|
|
570
|
+
if (normalized.endsWith('*'))
|
|
571
|
+
return path.startsWith(normalized.slice(0, -1));
|
|
572
|
+
return path === normalized;
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
function extractBearerToken(header) {
|
|
576
|
+
const match = header?.match(/^Bearer\s+(.+)$/i);
|
|
577
|
+
return match?.[1]?.trim() || '';
|
|
578
|
+
}
|
|
579
|
+
function timingSafeEqual(left, right) {
|
|
580
|
+
const leftBuffer = Buffer.from(left);
|
|
581
|
+
const rightBuffer = Buffer.from(right);
|
|
582
|
+
if (leftBuffer.length !== rightBuffer.length)
|
|
583
|
+
return false;
|
|
584
|
+
return (0, node_crypto_1.timingSafeEqual)(leftBuffer, rightBuffer);
|
|
585
|
+
}
|
|
586
|
+
function serializeBody(body, headers) {
|
|
587
|
+
if (body === undefined)
|
|
588
|
+
return undefined;
|
|
589
|
+
if (typeof body === 'string')
|
|
590
|
+
return body;
|
|
591
|
+
if (body instanceof URLSearchParams || body instanceof FormData || body instanceof Blob) {
|
|
592
|
+
return body;
|
|
593
|
+
}
|
|
594
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body))
|
|
595
|
+
return body;
|
|
596
|
+
if (!headers.has('Content-Type'))
|
|
597
|
+
headers.set('Content-Type', 'application/json');
|
|
598
|
+
return JSON.stringify(body);
|
|
599
|
+
}
|
|
600
|
+
function addQuery(url, query) {
|
|
601
|
+
if (!query)
|
|
602
|
+
return url;
|
|
603
|
+
const parsed = new URL(url);
|
|
604
|
+
for (const [key, value] of Object.entries(query)) {
|
|
605
|
+
if (value !== undefined && value !== null)
|
|
606
|
+
parsed.searchParams.set(key, String(value));
|
|
607
|
+
}
|
|
608
|
+
return parsed.toString();
|
|
609
|
+
}
|
|
610
|
+
async function parseResponse(response) {
|
|
611
|
+
const text = await readResponseText(response, MAX_INTERNAL_RESPONSE_BYTES);
|
|
612
|
+
if (!text)
|
|
613
|
+
return undefined;
|
|
614
|
+
try {
|
|
615
|
+
return JSON.parse(text);
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
return text;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async function readResponseText(response, maxBytes) {
|
|
622
|
+
const contentLength = Number(response.headers.get('content-length'));
|
|
623
|
+
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
|
|
624
|
+
throw new RangeError(`Resposta da mini API excedeu ${maxBytes} bytes.`);
|
|
625
|
+
}
|
|
626
|
+
if (!response.body)
|
|
627
|
+
return '';
|
|
628
|
+
const reader = response.body.getReader();
|
|
629
|
+
const chunks = [];
|
|
630
|
+
let total = 0;
|
|
631
|
+
try {
|
|
632
|
+
while (true) {
|
|
633
|
+
const { done, value } = await reader.read();
|
|
634
|
+
if (done)
|
|
635
|
+
break;
|
|
636
|
+
total += value.byteLength;
|
|
637
|
+
if (total > maxBytes) {
|
|
638
|
+
await reader.cancel();
|
|
639
|
+
throw new RangeError(`Resposta da mini API excedeu ${maxBytes} bytes.`);
|
|
640
|
+
}
|
|
641
|
+
chunks.push(value);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
finally {
|
|
645
|
+
reader.releaseLock();
|
|
646
|
+
}
|
|
647
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString('utf8');
|
|
648
|
+
}
|
|
649
|
+
async function closeHttpServer(server) {
|
|
650
|
+
await new Promise((resolve) => {
|
|
651
|
+
let finished = false;
|
|
652
|
+
const finish = () => {
|
|
653
|
+
if (finished)
|
|
654
|
+
return;
|
|
655
|
+
finished = true;
|
|
656
|
+
clearTimeout(timeout);
|
|
657
|
+
resolve();
|
|
658
|
+
};
|
|
659
|
+
const timeout = setTimeout(() => {
|
|
660
|
+
server.closeAllConnections?.();
|
|
661
|
+
(0, ConsoleLog_1.logWarn)(`Encerramento da mini API excedeu ${SERVER_CLOSE_TIMEOUT_MS}ms; conexões restantes foram finalizadas.`, 'Api');
|
|
662
|
+
finish();
|
|
663
|
+
}, SERVER_CLOSE_TIMEOUT_MS);
|
|
664
|
+
server.close(() => finish());
|
|
665
|
+
server.closeIdleConnections?.();
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
function isHttpStatus(error) {
|
|
669
|
+
return typeof error === 'object' && error !== null &&
|
|
670
|
+
'status' in error && typeof error.status === 'number' &&
|
|
671
|
+
error.status >= 400 && error.status <= 599;
|
|
672
|
+
}
|
|
673
|
+
function formatHost(host) {
|
|
674
|
+
return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
|
|
675
|
+
}
|
|
676
|
+
//# sourceMappingURL=ExpressApi.js.map
|