Upload 3 files
Browse files- package.json +1 -0
- server.js +42 -75
package.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
| 12 |
"express": "^4.21.2",
|
| 13 |
"express-rate-limit": "^7.1.5",
|
| 14 |
"helmet": "^7.1.0",
|
|
|
|
| 15 |
"node-fetch": "^2.7.0"
|
| 16 |
},
|
| 17 |
"engines": {
|
|
|
|
| 12 |
"express": "^4.21.2",
|
| 13 |
"express-rate-limit": "^7.1.5",
|
| 14 |
"helmet": "^7.1.0",
|
| 15 |
+
"jsonwebtoken": "^9.0.2",
|
| 16 |
"node-fetch": "^2.7.0"
|
| 17 |
},
|
| 18 |
"engines": {
|
server.js
CHANGED
|
@@ -4,6 +4,7 @@ const cors = require('cors');
|
|
| 4 |
const rateLimit = require('express-rate-limit');
|
| 5 |
const helmet = require('helmet');
|
| 6 |
const crypto = require('crypto');
|
|
|
|
| 7 |
require('dotenv').config();
|
| 8 |
|
| 9 |
const app = express();
|
|
@@ -253,17 +254,6 @@ function hasValidApiKey(req) {
|
|
| 253 |
return typeof apiKey === 'string' && API_KEYS.some((configuredKey) => timingSafeEquals(configuredKey, apiKey));
|
| 254 |
}
|
| 255 |
|
| 256 |
-
function base64UrlEncode(value) {
|
| 257 |
-
const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
| 258 |
-
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
function base64UrlDecode(value) {
|
| 262 |
-
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
|
| 263 |
-
const padLength = (4 - (normalized.length % 4)) % 4;
|
| 264 |
-
return Buffer.from(normalized + '='.repeat(padLength), 'base64');
|
| 265 |
-
}
|
| 266 |
-
|
| 267 |
function normalizeBotName(rawBotName) {
|
| 268 |
if (typeof rawBotName !== 'string') return '';
|
| 269 |
return rawBotName.toLowerCase().replace(/\s+/g, '-').substring(0, 100);
|
|
@@ -289,60 +279,30 @@ function hashUserAgent(value) {
|
|
| 289 |
return crypto.createHash('sha256').update(userAgent).digest('hex');
|
| 290 |
}
|
| 291 |
|
| 292 |
-
function createChatAccessToken(payload) {
|
| 293 |
-
const header = { alg: 'HS256', typ: 'JWT' };
|
| 294 |
-
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
| 295 |
-
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
| 296 |
-
const unsigned = `${encodedHeader}.${encodedPayload}`;
|
| 297 |
-
const signature = crypto.createHmac('sha256', CHAT_TOKEN_SECRET).update(unsigned).digest();
|
| 298 |
-
return `${unsigned}.${base64UrlEncode(signature)}`;
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
function verifyChatAccessToken(token) {
|
| 302 |
if (typeof token !== 'string' || token.length < 20) {
|
| 303 |
return { valid: false, reason: 'missing-token' };
|
| 304 |
}
|
| 305 |
|
| 306 |
-
const parts = token.split('.');
|
| 307 |
-
if (parts.length !== 3) {
|
| 308 |
-
return { valid: false, reason: 'invalid-format' };
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
| 312 |
-
let header;
|
| 313 |
let payload;
|
| 314 |
|
| 315 |
try {
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
return { valid: false, reason: '
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
crypto.createHmac('sha256', CHAT_TOKEN_SECRET).update(unsigned).digest()
|
| 329 |
-
);
|
| 330 |
-
|
| 331 |
-
if (!timingSafeEquals(expectedSignature, encodedSignature)) {
|
| 332 |
-
return { valid: false, reason: 'invalid-signature' };
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
const nowSeconds = Math.floor(Date.now() / 1000);
|
| 336 |
-
if (typeof payload.exp !== 'number' || payload.exp + CHAT_TOKEN_CLOCK_SKEW_SECONDS < nowSeconds) {
|
| 337 |
-
return { valid: false, reason: 'expired' };
|
| 338 |
-
}
|
| 339 |
-
|
| 340 |
-
if (typeof payload.iat !== 'number' || payload.iat - CHAT_TOKEN_CLOCK_SKEW_SECONDS > nowSeconds) {
|
| 341 |
-
return { valid: false, reason: 'invalid-issued-at' };
|
| 342 |
}
|
| 343 |
|
| 344 |
-
if (payload
|
| 345 |
-
return { valid: false, reason: 'invalid-
|
| 346 |
}
|
| 347 |
|
| 348 |
if (!Number.isInteger(payload.instanceNum) || payload.instanceNum < 1) {
|
|
@@ -632,14 +592,21 @@ setInterval(() => {
|
|
| 632 |
}, 10 * 60 * 1000);
|
| 633 |
|
| 634 |
// --- FETCH WITH TIMEOUT ---
|
| 635 |
-
async function fetchWithTimeout(url, options, timeout = 10000) {
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
)
|
| 641 |
-
|
| 642 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 643 |
|
| 644 |
// --- RESOLVE CHATFLOW ID ---
|
| 645 |
async function resolveChatflowId(instanceNum, botName) {
|
|
@@ -727,12 +694,12 @@ async function handleStreamingResponse(flowiseResponse, clientRes) {
|
|
| 727 |
}
|
| 728 |
}, 5000);
|
| 729 |
|
| 730 |
-
flowiseResponse.body.on('data', (chunk) => {
|
| 731 |
-
|
| 732 |
-
streamStarted = true;
|
| 733 |
-
dataReceived = true;
|
| 734 |
-
lastDataTime = Date.now();
|
| 735 |
-
totalBytes += chunk.length;
|
| 736 |
|
| 737 |
logSensitive(`[Streaming] Received chunk: ${chunk.length} bytes (total: ${totalBytes})`);
|
| 738 |
clientRes.write(chunk);
|
|
@@ -837,12 +804,7 @@ app.post('/auth/chat-token', tokenIssueLimiter, async (req, res) => {
|
|
| 837 |
});
|
| 838 |
}
|
| 839 |
|
| 840 |
-
const nowSeconds = Math.floor(Date.now() / 1000);
|
| 841 |
const claims = {
|
| 842 |
-
iss: CHAT_TOKEN_ISSUER,
|
| 843 |
-
iat: nowSeconds,
|
| 844 |
-
exp: nowSeconds + CHAT_TOKEN_TTL_SECONDS,
|
| 845 |
-
jti: crypto.randomBytes(12).toString('hex'),
|
| 846 |
instanceNum: target.instanceNum,
|
| 847 |
botName: target.botName
|
| 848 |
};
|
|
@@ -859,7 +821,12 @@ app.post('/auth/chat-token', tokenIssueLimiter, async (req, res) => {
|
|
| 859 |
claims.uaHash = hashUserAgent(req.headers['user-agent'] || '');
|
| 860 |
}
|
| 861 |
|
| 862 |
-
const token =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 863 |
res.status(200).json({
|
| 864 |
token,
|
| 865 |
tokenType: 'Bearer',
|
|
|
|
| 4 |
const rateLimit = require('express-rate-limit');
|
| 5 |
const helmet = require('helmet');
|
| 6 |
const crypto = require('crypto');
|
| 7 |
+
const jwt = require('jsonwebtoken');
|
| 8 |
require('dotenv').config();
|
| 9 |
|
| 10 |
const app = express();
|
|
|
|
| 254 |
return typeof apiKey === 'string' && API_KEYS.some((configuredKey) => timingSafeEquals(configuredKey, apiKey));
|
| 255 |
}
|
| 256 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
function normalizeBotName(rawBotName) {
|
| 258 |
if (typeof rawBotName !== 'string') return '';
|
| 259 |
return rawBotName.toLowerCase().replace(/\s+/g, '-').substring(0, 100);
|
|
|
|
| 279 |
return crypto.createHash('sha256').update(userAgent).digest('hex');
|
| 280 |
}
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
function verifyChatAccessToken(token) {
|
| 283 |
if (typeof token !== 'string' || token.length < 20) {
|
| 284 |
return { valid: false, reason: 'missing-token' };
|
| 285 |
}
|
| 286 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
let payload;
|
| 288 |
|
| 289 |
try {
|
| 290 |
+
payload = jwt.verify(token, CHAT_TOKEN_SECRET, {
|
| 291 |
+
algorithms: ['HS256'],
|
| 292 |
+
issuer: CHAT_TOKEN_ISSUER,
|
| 293 |
+
clockTolerance: CHAT_TOKEN_CLOCK_SKEW_SECONDS
|
| 294 |
+
});
|
| 295 |
+
} catch (error) {
|
| 296 |
+
const message = String(error && error.message ? error.message : '').toLowerCase();
|
| 297 |
+
if (message.includes('jwt expired')) return { valid: false, reason: 'expired' };
|
| 298 |
+
if (message.includes('invalid issuer')) return { valid: false, reason: 'invalid-issuer' };
|
| 299 |
+
if (message.includes('invalid signature')) return { valid: false, reason: 'invalid-signature' };
|
| 300 |
+
if (message.includes('jwt malformed') || message.includes('invalid token')) return { valid: false, reason: 'invalid-format' };
|
| 301 |
+
return { valid: false, reason: 'invalid-token' };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
}
|
| 303 |
|
| 304 |
+
if (!payload || typeof payload !== 'object') {
|
| 305 |
+
return { valid: false, reason: 'invalid-token' };
|
| 306 |
}
|
| 307 |
|
| 308 |
if (!Number.isInteger(payload.instanceNum) || payload.instanceNum < 1) {
|
|
|
|
| 592 |
}, 10 * 60 * 1000);
|
| 593 |
|
| 594 |
// --- FETCH WITH TIMEOUT ---
|
| 595 |
+
async function fetchWithTimeout(url, options, timeout = 10000) {
|
| 596 |
+
const controller = new AbortController();
|
| 597 |
+
const timer = setTimeout(() => controller.abort(), timeout);
|
| 598 |
+
|
| 599 |
+
try {
|
| 600 |
+
return await fetch(url, { ...options, signal: controller.signal });
|
| 601 |
+
} catch (error) {
|
| 602 |
+
if (error && error.name === 'AbortError') {
|
| 603 |
+
throw new Error('Request timeout');
|
| 604 |
+
}
|
| 605 |
+
throw error;
|
| 606 |
+
} finally {
|
| 607 |
+
clearTimeout(timer);
|
| 608 |
+
}
|
| 609 |
+
}
|
| 610 |
|
| 611 |
// --- RESOLVE CHATFLOW ID ---
|
| 612 |
async function resolveChatflowId(instanceNum, botName) {
|
|
|
|
| 694 |
}
|
| 695 |
}, 5000);
|
| 696 |
|
| 697 |
+
flowiseResponse.body.on('data', (chunk) => {
|
| 698 |
+
clearInterval(timeoutCheck);
|
| 699 |
+
streamStarted = true;
|
| 700 |
+
dataReceived = true;
|
| 701 |
+
lastDataTime = Date.now();
|
| 702 |
+
totalBytes += chunk.length;
|
| 703 |
|
| 704 |
logSensitive(`[Streaming] Received chunk: ${chunk.length} bytes (total: ${totalBytes})`);
|
| 705 |
clientRes.write(chunk);
|
|
|
|
| 804 |
});
|
| 805 |
}
|
| 806 |
|
|
|
|
| 807 |
const claims = {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 808 |
instanceNum: target.instanceNum,
|
| 809 |
botName: target.botName
|
| 810 |
};
|
|
|
|
| 821 |
claims.uaHash = hashUserAgent(req.headers['user-agent'] || '');
|
| 822 |
}
|
| 823 |
|
| 824 |
+
const token = jwt.sign(claims, CHAT_TOKEN_SECRET, {
|
| 825 |
+
algorithm: 'HS256',
|
| 826 |
+
issuer: CHAT_TOKEN_ISSUER,
|
| 827 |
+
expiresIn: CHAT_TOKEN_TTL_SECONDS,
|
| 828 |
+
jwtid: crypto.randomBytes(12).toString('hex')
|
| 829 |
+
});
|
| 830 |
res.status(200).json({
|
| 831 |
token,
|
| 832 |
tokenType: 'Bearer',
|