From a35869d6e82e558ba87d315f6ad7a32b4d129d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lobo=20Metal=C3=BArgico?= <43734867+LoboMetalurgico@users.noreply.github.com> Date: Sun, 2 Nov 2025 22:02:41 -0300 Subject: [PATCH 1/5] fix: Incorrect management of the masterKey --- api/src/interfaces/IMessage.ts | 3 +-- src/main/websocket/managers/messageHandler.ts | 15 ++++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/api/src/interfaces/IMessage.ts b/api/src/interfaces/IMessage.ts index 2646e45..bb4f556 100644 --- a/api/src/interfaces/IMessage.ts +++ b/api/src/interfaces/IMessage.ts @@ -8,8 +8,7 @@ export interface IMessage { id: string, key?: string }, - coreKey?: string, command?: string, args?: string[], - content: any + content: unknown } diff --git a/src/main/websocket/managers/messageHandler.ts b/src/main/websocket/managers/messageHandler.ts index ffb542f..5b2ec57 100644 --- a/src/main/websocket/managers/messageHandler.ts +++ b/src/main/websocket/managers/messageHandler.ts @@ -75,7 +75,6 @@ export class MessageHandler { if (message.from.key) delete message.from.key; if (message.target?.key) delete message.target.key; - if (message.coreKey) delete message.coreKey; targetConnection.send(message); } @@ -98,7 +97,7 @@ export class MessageHandler { connection.send(this.internalFormatToString('service-unavaliable', { command: '503', target: { id: message.from.id }, type: 'unavaliable' })); break; } - if (this.masterKey !== message.coreKey) { + if (this.masterKey !== message.content) { connection.send(this.internalFormatToString('unauthorized', { command: '401', target: { id: message.from.id } })); break; } @@ -118,14 +117,12 @@ export class MessageHandler { ( this.masterKey && ( - message.coreKey || ( - ( - typeof message.content === 'string' || - typeof message.content === typeof Array - ) && - message.content.includes(this.masterKey) - ) || + typeof message.content === 'string' || + typeof message.content === typeof Array + ) && + (message.content as (string | Array)).includes(this.masterKey) + || message.args?.includes(this.masterKey) ) ) From 52477f3b7531a4eae85162045796d82ac57c7928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lobo=20Metal=C3=BArgico?= <43734867+LoboMetalurgico@users.noreply.github.com> Date: Sun, 2 Nov 2025 22:04:00 -0300 Subject: [PATCH 2/5] chore(docs): Improved API TSDocs --- api/package-lock.json | 4 +- api/package.json | 2 +- api/src/websocket/WebSocketClient.ts | 108 +++++++++++++++------------ package-lock.json | 4 +- package.json | 2 +- 5 files changed, 67 insertions(+), 53 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index deb2ec6..e2b322b 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1,12 +1,12 @@ { "name": "arunacore-api", - "version": "1.0.0-BETA.4", + "version": "1.0.0-BETA.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "arunacore-api", - "version": "1.0.0-BETA.4", + "version": "1.0.0-BETA.5", "license": "GPL-3.0", "dependencies": { "@promisepending/logger.js": "^1.1.1", diff --git a/api/package.json b/api/package.json index 24fa570..ca615b0 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "arunacore-api", - "version": "1.0.0-BETA.4", + "version": "1.0.0-BETA.5", "description": "ArunaCore is an open source websocket server made with nodejs for intercomunication between applications.", "main": "build/src/index.js", "types": "build/src/index.d.ts", diff --git a/api/src/websocket/WebSocketClient.ts b/api/src/websocket/WebSocketClient.ts index 8353463..0681970 100644 --- a/api/src/websocket/WebSocketClient.ts +++ b/api/src/websocket/WebSocketClient.ts @@ -16,27 +16,33 @@ interface ArunaEvents { } /** - * Main class for the api client - * @class ArunaClient - * @extends {EventEmitter} + * The main WebSocket client for connecting to an ArunaCore server. + * Handles connection, authentication, messaging, and event management. + * + * @remarks + * This client supports secure and shard modes, emits events for connection lifecycle and message handling, + * and provides methods for sending messages, pinging, and closing the connection. + * + * @extends EventEmitter + * * @example - * const { ArunaClient } = require('aruna-api'); + * import { ArunaClient } from 'aruna-api'; * const client = new ArunaClient({ - * host: 'localhost', - * port: 3000, - * secureMode: false, - * shardMode: false, - * secureKey: null, - * logger: null, - * id: 'client' + * host: 'localhost', + * port: 3000, + * secureMode: false, + * shardMode: false, + * secureKey: null, + * logger: null, + * id: 'client' * }); * client.on('ready', () => { - * console.log('Client is ready!'); + * console.log('Client is ready!'); * }); * client.on('message', (message) => { - * console.log(message); + * console.log(message); * }); - * client.connect(); // optional: secureKey + * await client.connect(); // Optionally pass secureKey */ export class ArunaClient extends EventEmitter { private finishTimeout: ReturnType | null = null; @@ -69,15 +75,18 @@ export class ArunaClient extends EventEmitter { } /** - * Connects to the ArunaCore server - * @param {string?} [secureKey] Secure key for secure mode - * @returns {Promise} + * Establishes a WebSocket connection to the ArunaCore server. + * + * @param {string?} [secureKey] Optional secure key for authentication in secure mode. If provided, enables secure mode. + * @returns {Promise} Promise that resolves when the connection is established, or rejects on error. + * @throws Error if secure mode is enabled but no secure key is provided, or if shard mode is enabled without secure mode. */ public async connect(secureKey?: string): Promise { if (secureKey) { this.secureKey = secureKey; this.secureMode = true; } + this.ws = new ws(`ws://${this.host}:${this.port}`, { headers: { 'Authorization': this.secureKey ?? '', @@ -96,12 +105,12 @@ export class ArunaClient extends EventEmitter { this.ws!.on('open', () => { this.logger.debug('Connected to server!'); this.emit('ready'); - resolve(); + return resolve(); }); this.ws!.on('error', (err) => { this.logger.error(err); - reject(err); + return reject(err); }); this.ws!.on('unexpected-response', (req, res) => { @@ -113,18 +122,18 @@ export class ArunaClient extends EventEmitter { this.logger.warn('Conflict: Client ID already connected! Randomizing a new one...'); this.id = this.id + utils.randomString(5); this.connect().then(() => { - resolve(); + return resolve(); }).catch((err) => { reject(err); }); } else if (res.statusCode === 412) { this.logger.error('Precondition Failed: Unsupported API version!'); this.ws?.close(); - reject(new Error('Precondition Failed: Unsupported API version!')); + return reject(new Error('Precondition Failed: Unsupported API version!')); } else { this.logger.error(`Unexpected response: ${res.statusCode}`); this.ws?.close(); - reject(new Error(`Unexpected response: ${res.statusCode}`)); + return reject(new Error(`Unexpected response: ${res.statusCode}`)); } }); }); @@ -132,15 +141,15 @@ export class ArunaClient extends EventEmitter { /** * Sends a message to the ArunaCore server. - * - * @param {any} content - Content of the message, can be a string or a serializable object. - * @param {Object} options - Options for the message. - * @param {string} [options.type] - Type of the client or message. - * @param {string} [options.command] - Command code for the message. - * @param {{ id: string, key?: string }} [options.target] - Target of the message (id and optional key). - * @param {string[]} [options.args] - Arguments of the message. - * @returns {Promise} Resolves when the message is sent. - * + * + * @param {unknown} content The message content, can be a string or a serializable object. + * @param {Object} options Message options. + * @param {string} [options.type] Type of the client or message. + * @param {string} [options.command] Command code for the message. + * @param {{ id: string, key?: string }} [options.target] Target recipient (id and optional key). + * @param {string[]} [options.args] Arguments for the message. + * @returns {Promise} Promise that resolves when the message is sent, or rejects if the connection is not open or secure key is missing in secure mode. + * * @example * await client.send('100', { * args: ['Hello World!'], @@ -149,7 +158,7 @@ export class ArunaClient extends EventEmitter { * command: 'someCommand' * }); */ - public async send(content: any, { type, command, target, args }: { type?: string, command?: string, target?: { id: string, key?: string }, args?: string[] }): Promise { + public async send(content: unknown, { type, command, target, args }: { type?: string, command?: string, target?: { id: string, key?: string }, args?: string[] }): Promise { return new Promise((resolve, reject) => { if (this.ws?.readyState !== ws.OPEN) return reject(new Error('Connection is not open!')); @@ -172,9 +181,10 @@ export class ArunaClient extends EventEmitter { } /** - * Called when a message is received from the ArunaCore server - * Responsable for parsing the message and emitting the events - * @param {string} message Message received + * Handles incoming messages from the ArunaCore server. + * Parses the message and emits the appropriate events. + * + * @param {string} message The raw message string received from the server. * @returns {void} * @private */ @@ -208,12 +218,13 @@ export class ArunaClient extends EventEmitter { } /** - * Pings the ArunaCore server - * @returns {Promise} + * Sends a ping frame to the ArunaCore server to check connectivity. + * + * @returns {Promise} Promise that resolves to true if the ping succeeds, or rejects on error. + * * @example - * client.ping().then(() => { - * console.log('Pong!'); - * }); + * await client.ping(); + * // => Pong! */ public async ping(): Promise { return new Promise((resolve, reject) => { @@ -230,20 +241,23 @@ export class ArunaClient extends EventEmitter { } /** - * Returns the client id - * @returns {string} + * Gets the current client ID. + * + * @returns {string} The client ID string. */ public getID(): string { return this.id; } /** - * Closes the connection to the ArunaCore server - * @returns {Promise} + * Gracefully closes the connection to the ArunaCore server. + * Sends an unregister request and waits for confirmation or times out after 5 seconds. + * + * @returns {Promise} Promise that resolves when the connection is closed. + * * @example - * client.finish().then(() => { - * console.log('Connection closed!'); - * }); + * await client.finish(); + * // => Connection closed! */ public async finish(): Promise { return new Promise(async (resolve) => { diff --git a/package-lock.json b/package-lock.json index dce5531..3d9b277 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "arunacore", - "version": "1.0.0-BETA.4", + "version": "1.0.0-BETA.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "arunacore", - "version": "1.0.0-BETA.4", + "version": "1.0.0-BETA.5", "hasInstallScript": true, "license": "GPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 5ca0cc6..4face92 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "arunacore", - "version": "1.0.0-BETA.4", + "version": "1.0.0-BETA.5", "description": "ArunaCore is an open source websocket server made with nodejs for intercomunication between applications.", "main": "build/nodejs/src/main/start.js", "type": "module", From df5c4004219a84e73b9336a8a63e75b658c48b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lobo=20Metal=C3=BArgico?= <43734867+LoboMetalurgico@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:59:48 -0300 Subject: [PATCH 3/5] feat: implement request method --- CHANGELOG.md | 12 ++ api/src/interfaces/IMessage.ts | 2 + api/src/interfaces/IWebsocketOptions.ts | 21 ++- api/src/websocket/WebSocketClient.ts | 194 ++++++++++++++++++++---- api/src/websocket/parser.ts | 9 ++ src/tests/scripts/sendRequest.ts | 36 +++++ 6 files changed, 239 insertions(+), 35 deletions(-) create mode 100644 src/tests/scripts/sendRequest.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1019532..0cb4789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # ArunaCore Changelog +## v1.0.0-BETA.5 + - [FEAT] Split `401 Unauthorized` into `401 Unauthorized` and `403 Forbidden` + - Now, `401` is used when authentication fails (e.g., invalid secure key), while `403` is used when access is denied despite valid authentication (e.g., insufficient permissions, invalid key for specific target communication, invalid masterkey, etc.). + - This provides clearer feedback to clients about the nature of access issues. + + - [FEAT] Implemented `client.request()` method in `WebSocketClient` + - This method allows sending a message and awaiting a response, simplifying request-response interactions. + - To use it, simply call `client.request(message)` and await the returned promise. + - To respond to such request on the other side, handle the `request` event and reply using `message.reply`. + + - [CHORE] Improved code documentation and comments for better clarity and maintainability. + ## v1.0.0-BETA.4 - [BREAKING] Dropped support for all versions below Node.js v20.11.1 diff --git a/api/src/interfaces/IMessage.ts b/api/src/interfaces/IMessage.ts index bb4f556..a308b7e 100644 --- a/api/src/interfaces/IMessage.ts +++ b/api/src/interfaces/IMessage.ts @@ -8,7 +8,9 @@ export interface IMessage { id: string, key?: string }, + uuid?: string, command?: string, args?: string[], content: unknown + reply?: (content: unknown, options?: { args?: string[], toKey?: string }) => Promise; } diff --git a/api/src/interfaces/IWebsocketOptions.ts b/api/src/interfaces/IWebsocketOptions.ts index 99107f9..c8a74bb 100644 --- a/api/src/interfaces/IWebsocketOptions.ts +++ b/api/src/interfaces/IWebsocketOptions.ts @@ -1,11 +1,18 @@ import { Logger } from '@promisepending/logger.js'; +export interface IReconnectionOptions { + enabled: boolean, + maxAttempts?: number, + delay?: number +} + export interface IWebsocketOptions { - host: string - port: number - id: string - secureMode?: boolean - secureKey?: string - shardMode?: boolean - logger?: Logger + host: string + port: number + id: string + secureMode?: boolean + secureKey?: string + shardMode?: boolean + logger?: Logger + reconnection?: IReconnectionOptions } diff --git a/api/src/websocket/WebSocketClient.ts b/api/src/websocket/WebSocketClient.ts index 0681970..bd51950 100644 --- a/api/src/websocket/WebSocketClient.ts +++ b/api/src/websocket/WebSocketClient.ts @@ -1,13 +1,16 @@ -import { IMessage, IWebsocketOptions } from '../interfaces'; +import { IMessage, IReconnectionOptions, IWebsocketOptions } from '../interfaces'; +import { setTimeout, setInterval, clearInterval, clearTimeout } from 'timers'; import { Logger } from '@promisepending/logger.js'; import { version } from '../resources/version'; import { WebSocketParser, utils } from '../'; import { EventEmitter } from 'events'; +import { randomUUID } from 'crypto'; import ws from 'ws'; interface ArunaEvents { 'ready': []; 'message': [IMessage]; + 'request': [IMessage]; 'unauthorized': [IMessage]; 'error': [Error]; 'close': [number, string]; @@ -45,7 +48,10 @@ interface ArunaEvents { * await client.connect(); // Optionally pass secureKey */ export class ArunaClient extends EventEmitter { + private reconnectionConfig: IReconnectionOptions = { enabled: true, maxAttempts: -1, delay: 5000 }; + private reconnectionLoop: ReturnType | null = null; private finishTimeout: ReturnType | null = null; + private reconnectionAttempts: number = 0; private secureKey: string | null = null; private ws: ws | null = null; private secureMode: boolean; @@ -72,6 +78,7 @@ export class ArunaClient extends EventEmitter { this.shardMode = options.shardMode ?? false; if (this.shardMode && !this.secureMode) throw new Error('Shard mode requires secure mode!'); + this.reconnectionConfig = options.reconnection ? { ...this.reconnectionConfig, ...options.reconnection } : this.reconnectionConfig; } /** @@ -86,7 +93,7 @@ export class ArunaClient extends EventEmitter { this.secureKey = secureKey; this.secureMode = true; } - + this.ws = new ws(`ws://${this.host}:${this.port}`, { headers: { 'Authorization': this.secureKey ?? '', @@ -100,7 +107,14 @@ export class ArunaClient extends EventEmitter { this.ws!.on('close', (code, reason) => { this.emit('close', code, reason.toString()); }); - this.ws!.on('error', (err) => { this.emit('error', err); }); + this.ws!.on('error', (err: any) => { + if (err.code === 'ECONNREFUSED' && this.reconnectionConfig.enabled) { + this.logger.warn(`Cannot connect to server. Retrying in ${this.reconnectionConfig.delay}ms...`); + this.startReconnectionLoop(); + return; + } + this.emit('error', err); + }); this.ws!.on('open', () => { this.logger.debug('Connected to server!'); @@ -108,37 +122,64 @@ export class ArunaClient extends EventEmitter { return resolve(); }); - this.ws!.on('error', (err) => { - this.logger.error(err); - return reject(err); - }); - this.ws!.on('unexpected-response', (req, res) => { - if (res.statusCode === 401) { - this.logger.error('Unauthorized: Invalid secure key!'); - this.ws?.close(); - reject(new Error('Unauthorized: Invalid secure key!')); - } else if (res.statusCode === 409) { - this.logger.warn('Conflict: Client ID already connected! Randomizing a new one...'); - this.id = this.id + utils.randomString(5); - this.connect().then(() => { - return resolve(); - }).catch((err) => { - reject(err); - }); - } else if (res.statusCode === 412) { - this.logger.error('Precondition Failed: Unsupported API version!'); - this.ws?.close(); - return reject(new Error('Precondition Failed: Unsupported API version!')); - } else { - this.logger.error(`Unexpected response: ${res.statusCode}`); - this.ws?.close(); - return reject(new Error(`Unexpected response: ${res.statusCode}`)); + switch (res.statusCode) { + case 401: + this.logger.error('Unauthorized: Invalid secure key!'); + this.ws!.close(); + return reject(new Error('Unauthorized: Invalid secure key!')); + case 409: + this.logger.warn('Conflict: Client ID already connected! Randomizing a new one...'); + this.id = this.id + utils.randomString(5); + this.startReconnectionLoop(); + return; + case 412: + this.logger.error('Precondition Failed: Unsupported API version!'); + this.ws!.close(); + return reject(new Error('Precondition Failed: Unsupported API version!')); + default: + this.logger.error(`Unexpected response: ${res.statusCode}`); + this.ws!.close(); + return reject(new Error(`Unexpected response: ${res.statusCode}`)); } }); }); } + private startReconnectionLoop(): void { + if (this.reconnectionLoop) return; + + this.reconnectionLoop = setInterval(async () => { + this.ws?.terminate(); + this.ws?.removeAllListeners(); + this.ws = null; + if (this.reconnectionConfig.maxAttempts !== -1 && this.reconnectionAttempts >= this.reconnectionConfig.maxAttempts!) { + this.logger.error('Max reconnection attempts reached. Stopping reconnection attempts.'); + this.stopReconnectionLoop(); + return; + } + + this.reconnectionAttempts += 1; + this.logger.info(`Reconnection attempt ${this.reconnectionAttempts}...`); + + try { + await this.connect(this.secureKey ?? undefined); + this.logger.info('Reconnected successfully!'); + this.stopReconnectionLoop(); + this.reconnectionAttempts = 0; + } catch (err) { + this.logger.error(`Reconnection attempt ${this.reconnectionAttempts} failed:`, err); + } + }, this.reconnectionConfig.delay); + } + + private stopReconnectionLoop(): void { + if (this.reconnectionLoop) { + clearInterval(this.reconnectionLoop); + this.reconnectionLoop = null; + } + } + /** * Sends a message to the ArunaCore server. * @@ -180,6 +221,61 @@ export class ArunaClient extends EventEmitter { }); } + + /** + * Sends a request to a specific target and waits for a response. + * + * @param {unknown} content The message content, can be a string or a serializable object. + * @param {Object} options Request options. + * @param {{ id: string, key?: string }} options.target Target recipient (id and optional key). + * @param {number} [options.timeoutMs=10000] Timeout in milliseconds to wait for the response. + * @returns {Promise} Promise that resolves with the response message or rejects on error or timeout. + * + * @throws {Error} If the secure key is required and not set, or if the connection is not open. + * + * @example + * const response = await client.request('ping', { + * target: { id: 'server', key: 'serverKey' }, + * timeoutMs: 5000 + * }); + * console.log(response); + */ + public async request(content: unknown, { target, timeoutMs = 10000 }: { target: { id: string, key?: string }, timeoutMs?: number }): Promise { + return new Promise(async (resolve, reject) => { + const uuid = randomUUID(); + const finalFrom: { id: string, key?: string } = { + id: this.id, + }; + + if (this.secureMode && this.secureKey == null) return reject(new Error('Secure key is required for secure mode!')); + else if (this.secureKey) Object.assign(finalFrom, { key: this.secureKey }); + + const onResponse = (message: IMessage): void => { + if (message.uuid === uuid) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + if (timeout) clearTimeout(timeout); + resolve(message); + } + }; + + const timeout = setTimeout(() => { + this.removeListener(`reply-${uuid}`, onResponse); + reject(new Error('Request timed out')); + }, timeoutMs); + + this.once(`reply-${uuid}`, onResponse); + + this.ws!.send(WebSocketParser.formatToStringWithUUID(finalFrom, uuid, content, { type: 'request', target }), (err) => { + if (err) { + this.logger.error(err); + this.removeListener(`reply-${uuid}`, onResponse); + if (timeout) clearTimeout(timeout); + reject(err); + } + }); + }); + } + /** * Handles incoming messages from the ArunaCore server. * Parses the message and emits the appropriate events. @@ -198,6 +294,48 @@ export class ArunaClient extends EventEmitter { if (!parsedMessage) return; + if (parsedMessage.uuid && parsedMessage.type === 'reply') { + this.emit(`reply-${parsedMessage.uuid}`, parsedMessage); + return; + } else if (parsedMessage.uuid && parsedMessage.type === 'request') { + parsedMessage.reply = (content: unknown, options?: { args?: string[], toKey?: string }): Promise => { + const { args, toKey } = options || {}; + return new Promise((resolve, reject) => { + const finalFrom: { id: string, key?: string } = { + id: this.id, + }; + + if (this.secureMode && this.secureKey == null) { + return reject(new Error('Secure key is required for secure mode!')); + } else if (this.secureKey) Object.assign(finalFrom, { key: this.secureKey }); + + const finalTarget: { id: string, key?: string } = { + id: parsedMessage!.from.id, + }; + + if (toKey) Object.assign(finalTarget, { key: toKey }); + + this.ws!.send( + WebSocketParser.formatToStringWithUUID( + finalFrom, parsedMessage!.uuid!, content, + { + type: 'reply', + target: finalTarget, + args, + }, + ), (err) => { + if (err) { + this.logger.error(err); + reject(err); + } else { + resolve(); + } + }); + }); + }; + this.emit('request', parsedMessage); + } + switch (parsedMessage.command) { case '000': if (parsedMessage.content === 'unregister-success') { diff --git a/api/src/websocket/parser.ts b/api/src/websocket/parser.ts index 8b73140..5beef2d 100644 --- a/api/src/websocket/parser.ts +++ b/api/src/websocket/parser.ts @@ -43,4 +43,13 @@ export class WebSocketParser { public static formatToString(from: { id: string, key?: string }, content: any, { type, command, target, args }: { type?: string, command?: string, target?: { id: string, key?: string }, args?: string[] }): string { return JSON.stringify(this.format(from, content, { type, command, target, args })); } + + /** + * @private + */ + public static formatToStringWithUUID(from: { id: string, key?: string }, uuid: string, content: any, { type, command, target, args }: { type?: string, command?: string, target?: { id: string, key?: string }, args?: string[] }): string { + const message = this.format(from, content, { type, command, target, args }); + Object.assign(message, { uuid }); + return JSON.stringify(message); + } } diff --git a/src/tests/scripts/sendRequest.ts b/src/tests/scripts/sendRequest.ts new file mode 100644 index 0000000..c2d573f --- /dev/null +++ b/src/tests/scripts/sendRequest.ts @@ -0,0 +1,36 @@ +import { IMessage } from 'arunacore-api'; +import { ITestOptions } from '../interfaces'; + +async function sendRequestTest({ loggerClient, client2, client3 }: ITestOptions): Promise { + return new Promise(async (resolve, reject) => { + const timeout = setTimeout(() => { + loggerClient.error('Request Timeout'); + return reject(new Error('Request Timeout')); + }, 10000); + + client3.once('request', async (message: IMessage) => { + loggerClient.info('Client 3 Received Request: ', message); + try { + await message.reply!('test', { toKey: 'test2' }); + loggerClient.info('Client 3 Replied to Request'); + } catch (e) { + loggerClient.error('Client 3 Failed to Reply to Request: ' + e); + return reject(e); + } + }); + + const response = await client2.request('test', { target: { id: 'client3', key: 'test3' } }); + loggerClient.info('Request Response: ', response); + clearTimeout(timeout); + + if (response.content === 'test') { + return resolve(); + } else { + return reject(new Error(`Invalid Response Content: ${response.content}`)); + } + }); +} + +export const name = 'Request Test'; +export const run = sendRequestTest; +export const order = 8; From f842ad64e55bfdee882a5f49d741e24658a1d5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lobo=20Metal=C3=BArgico?= <43734867+LoboMetalurgico@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:00:15 -0300 Subject: [PATCH 4/5] feat: split 401 into 401 and 403 --- api/src/websocket/WebSocketClient.ts | 3 +++ src/main/websocket/managers/messageHandler.ts | 11 +++-------- src/main/websocket/socket.ts | 2 +- src/tests/scripts/sendMessage3.ts | 4 ++-- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/api/src/websocket/WebSocketClient.ts b/api/src/websocket/WebSocketClient.ts index bd51950..2eb0aff 100644 --- a/api/src/websocket/WebSocketClient.ts +++ b/api/src/websocket/WebSocketClient.ts @@ -348,6 +348,9 @@ export class ArunaClient extends EventEmitter { case '401': this.emit('unauthorized', parsedMessage); break; + case '403': + this.emit('forbidden', parsedMessage); + break; default: this.emit('message', parsedMessage); if (parsedMessage.command) this.emit(parsedMessage.command, parsedMessage); diff --git a/src/main/websocket/managers/messageHandler.ts b/src/main/websocket/managers/messageHandler.ts index 5b2ec57..53a1c05 100644 --- a/src/main/websocket/managers/messageHandler.ts +++ b/src/main/websocket/managers/messageHandler.ts @@ -29,13 +29,8 @@ export class MessageHandler { const fromConnection = this.connectionManager.getConnection(message.from.id); - if (!fromConnection && message.type === 'register') { - connection.close(1000, this.internalFormatToString('deprecated-register-method', { command: '410', target: { id: message.from.id }, type: 'disconnect' })); - return; - } - if (!fromConnection) { - connection.send(this.internalFormatToString('unprocessable-entity', { command: '422', target: { id: message.from.id }, type: 'register' })); + connection.close(1000, this.internalFormatToString('unauthorized', { command: '401', target: { id: message.from.id }, type: 'disconnect' })); return; } @@ -67,7 +62,7 @@ export class MessageHandler { } if (targetConnection.getIsSecure() && targetConnection.getSecureKey() !== message.target?.key) { - connection.send(this.internalFormatToString('unauthorized', { command: '401', target: { id: message.from.id } })); + connection.send(this.internalFormatToString('forbidden', { command: '403', target: { id: message.from.id } })); return; } @@ -98,7 +93,7 @@ export class MessageHandler { break; } if (this.masterKey !== message.content) { - connection.send(this.internalFormatToString('unauthorized', { command: '401', target: { id: message.from.id } })); + connection.send(this.internalFormatToString('forbidden', { command: '403', target: { id: message.from.id } })); break; } var ids: string[] = Array.from(this.connectionManager.getAliveConnections().keys()); diff --git a/src/main/websocket/socket.ts b/src/main/websocket/socket.ts index 2cd3f8c..eee511e 100644 --- a/src/main/websocket/socket.ts +++ b/src/main/websocket/socket.ts @@ -107,7 +107,7 @@ export class Socket extends EventEmitter { id: appId, isAlive: true, connection: client, - apiVersion: apiVersion, // TODO: Create a good way to check if the client is using a supported api version + apiVersion: apiVersion, isSecure: !!userToken, secureKey: userToken, isSharded: false, // TODO: Implement sharding diff --git a/src/tests/scripts/sendMessage3.ts b/src/tests/scripts/sendMessage3.ts index 0cfea74..6c830ac 100644 --- a/src/tests/scripts/sendMessage3.ts +++ b/src/tests/scripts/sendMessage3.ts @@ -11,8 +11,8 @@ async function sendMessage3Test({ loggerClient, client, client2 }: ITestOptions) clearTimeout(timeout); return reject(new Error('Message 3 Received')); }); - client.once('unauthorized', () => { - loggerClient.info('Client 1 Unauthorized as intended'); + client.once('forbidden', () => { + loggerClient.info('Client 1 Forbidden as intended'); clearTimeout(timeout); return resolve(); }); From 61524a1410455fc70fcb1f762c37f54e794fec8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lobo=20Metal=C3=BArgico?= <43734867+LoboMetalurgico@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:32:12 -0300 Subject: [PATCH 5/5] chore: update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cb4789..430d420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # ArunaCore Changelog ## v1.0.0-BETA.5 + + - [FIX] Wrong management of `coreKey` + - Fixed an issue where the `coreKey` was handled incorrectly, causing it to be always null, resulting in clients being unable to access restricted resources. + - [FEAT] Split `401 Unauthorized` into `401 Unauthorized` and `403 Forbidden` - Now, `401` is used when authentication fails (e.g., invalid secure key), while `403` is used when access is denied despite valid authentication (e.g., insufficient permissions, invalid key for specific target communication, invalid masterkey, etc.). - This provides clearer feedback to clients about the nature of access issues.