Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

votifier-x

Package Overview
Dependencies
Maintainers
3
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

votifier-x - npm Package Compare versions

Comparing version
0.1.0
to
0.1.1
+88
dist/index.d.mts
import { EventEmitter } from 'events';
import { Server } from 'net';
interface VoteOptions {
username: string;
address: string;
serviceName?: string;
timestamp?: number;
additionalData?: string | Buffer;
}
interface VotifierPayload {
serviceName: string;
username: string;
address: string;
timestamp: number;
challenge: string;
additionalData?: string;
}
interface VotifierMessage {
payload: string;
signature: string;
}
type VotifierResponse = {
status: 'error';
cause: string;
error: string;
} | {
status: 'ok';
};
interface VotifierClientOptions {
token: string;
serviceName: string;
host: string;
/** Defaults to 8192 */
port?: number;
protocol?: 2;
debug?: boolean;
}
declare class VotifierClient {
readonly host: string;
readonly port: number;
readonly token: string;
readonly protocolVersion: number;
serviceName: string;
readonly debug: boolean;
constructor(options: VotifierClientOptions);
private getToken;
sendVote(vote: VoteOptions): Promise<void>;
}
declare const magicV2 = 29498;
declare class TokenManager {
private readonly tokenPath;
private tokens;
constructor(tokenPath: string);
private load;
private writeTokens;
getToken(serviceName: string): string | undefined;
setToken(serviceName: string, token: string): void;
generateToken(): string;
}
interface VotifierServerOptions {
/** Path of the token file. Defaults to 'tokens.json' */
tokenPath?: string;
/** Port to listen vote requests. Defaults to 8192 */
port?: number;
}
declare class VotifierServer extends EventEmitter {
readonly server: Server;
readonly options: VotifierServerOptions;
readonly protocolVersion: number;
readonly tokenManager: TokenManager;
constructor(options?: VotifierServerOptions);
private handleConnection;
private decode;
start(): void;
stop(): void;
}
interface VotifierServer {
on(event: 'listen', listener: () => void): this;
on(event: 'vote', listener: (vote: VotifierPayload) => void): this;
on(event: 'error', listener: (error: any) => void): this;
}
export { TokenManager, type VoteOptions, VotifierClient, type VotifierClientOptions, type VotifierMessage, type VotifierPayload, type VotifierResponse, VotifierServer, type VotifierServerOptions, magicV2 };
import { EventEmitter } from 'events';
import { Server } from 'net';
interface VoteOptions {
username: string;
address: string;
serviceName?: string;
timestamp?: number;
additionalData?: string | Buffer;
}
interface VotifierPayload {
serviceName: string;
username: string;
address: string;
timestamp: number;
challenge: string;
additionalData?: string;
}
interface VotifierMessage {
payload: string;
signature: string;
}
type VotifierResponse = {
status: 'error';
cause: string;
error: string;
} | {
status: 'ok';
};
interface VotifierClientOptions {
token: string;
serviceName: string;
host: string;
/** Defaults to 8192 */
port?: number;
protocol?: 2;
debug?: boolean;
}
declare class VotifierClient {
readonly host: string;
readonly port: number;
readonly token: string;
readonly protocolVersion: number;
serviceName: string;
readonly debug: boolean;
constructor(options: VotifierClientOptions);
private getToken;
sendVote(vote: VoteOptions): Promise<void>;
}
declare const magicV2 = 29498;
declare class TokenManager {
private readonly tokenPath;
private tokens;
constructor(tokenPath: string);
private load;
private writeTokens;
getToken(serviceName: string): string | undefined;
setToken(serviceName: string, token: string): void;
generateToken(): string;
}
interface VotifierServerOptions {
/** Path of the token file. Defaults to 'tokens.json' */
tokenPath?: string;
/** Port to listen vote requests. Defaults to 8192 */
port?: number;
}
declare class VotifierServer extends EventEmitter {
readonly server: Server;
readonly options: VotifierServerOptions;
readonly protocolVersion: number;
readonly tokenManager: TokenManager;
constructor(options?: VotifierServerOptions);
private handleConnection;
private decode;
start(): void;
stop(): void;
}
interface VotifierServer {
on(event: 'listen', listener: () => void): this;
on(event: 'vote', listener: (vote: VotifierPayload) => void): this;
on(event: 'error', listener: (error: any) => void): this;
}
export { TokenManager, type VoteOptions, VotifierClient, type VotifierClientOptions, type VotifierMessage, type VotifierPayload, type VotifierResponse, VotifierServer, type VotifierServerOptions, magicV2 };
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
TokenManager: () => TokenManager,
VotifierClient: () => VotifierClient,
VotifierServer: () => VotifierServer,
magicV2: () => magicV2
});
module.exports = __toCommonJS(index_exports);
// src/client/client.ts
var import_node_fs = require("fs");
var import_node_net = require("net");
var import_node_crypto = require("crypto");
// src/constants.ts
var magicV2 = 29498;
// src/client/client.ts
var VotifierClient = class {
host;
port = 8192;
token;
protocolVersion = 2;
serviceName;
debug = false;
constructor(options) {
this.token = options.token;
this.serviceName = options.serviceName;
this.host = options.host;
if (options.port) this.port = options.port;
if (options.protocol) this.protocolVersion = options.protocol;
if (options.debug) this.debug = options.debug;
}
getToken(path) {
if (!path) throw new Error("Token or token path is required");
return (0, import_node_fs.readFileSync)(path, "utf-8");
}
sendVote(vote) {
return new Promise((resolve, reject) => {
const socket = (0, import_node_net.createConnection)(this.port, this.host);
socket.setTimeout(5e3, () => {
socket.destroy();
handleError(new Error("Connection timeout"));
});
const handleError = (error) => {
socket.destroy();
socket.removeAllListeners();
reject(error);
};
const handleGreeting = (data) => {
const greeting = data.toString();
if (this.debug) console.log("[S->C] Greeting:", greeting);
const headers = greeting.split(" ");
if (headers.length !== 3) {
return handleError(new Error("Not a v2 protocol server"));
}
const payload = {
username: vote.username,
address: vote.address,
timestamp: vote.timestamp ?? Date.now(),
serviceName: vote.serviceName ?? this.serviceName,
challenge: headers[2].trim(),
additionalData: (typeof vote.additionalData === "string" ? Buffer.from(vote.additionalData) : vote.additionalData)?.toString("base64")
};
if (this.debug) console.log("[C->S] Payload:", payload, payload.challenge.length);
const serializedPayload = JSON.stringify(payload);
const signature = (0, import_node_crypto.createHmac)("sha256", this.token).update(serializedPayload).digest("base64");
const message = {
payload: serializedPayload,
signature
};
const serializedMessage = JSON.stringify(message);
const buffer = Buffer.alloc(4 + serializedMessage.length);
buffer.writeUInt16BE(magicV2, 0);
buffer.writeUInt16BE(serializedMessage.length, 2);
buffer.write(serializedMessage, 4);
socket.write(new Uint8Array(buffer));
socket.once("data", handleResponse);
};
const handleResponse = (data) => {
let response;
try {
response = JSON.parse(data.toString());
} catch (error) {
throw new Error("Failed to parse response");
}
if (this.debug) console.log("[S->C] Response:", response);
socket.end();
socket.removeAllListeners();
if (response.status === "ok") {
resolve();
} else if (response.status === "error") {
const err = new Error(response.error);
err.name = response.cause;
handleError(err);
} else {
handleError(new Error("Unknown response"));
}
};
socket.once("data", handleGreeting);
socket.once("error", handleError);
});
}
};
// src/server/server.ts
var import_events = require("events");
var import_net = require("net");
// src/server/tokens.ts
var crypto = __toESM(require("crypto"));
var import_node_fs2 = require("fs");
var import_node_path = require("path");
var TokenManager = class {
tokenPath;
tokens = /* @__PURE__ */ new Map();
constructor(tokenPath) {
this.tokenPath = tokenPath;
this.load();
}
load() {
if ((0, import_node_fs2.existsSync)(this.tokenPath)) {
const file = (0, import_node_fs2.readFileSync)(this.tokenPath, "utf-8");
this.tokens = new Map(Object.entries(JSON.parse(file)));
} else {
const defaultToken = this.generateToken();
this.tokens.set("default", defaultToken);
this.writeTokens();
console.log("-".repeat(75));
console.log("[VotifierX]");
console.log("No tokens were found in your tokenPath, so we've generated one for you.");
console.log("Your default Votifier token is " + defaultToken);
console.log("You will need to provide this token when you submit your server to a voting");
console.log("list.");
console.log("-".repeat(75));
}
}
writeTokens() {
(0, import_node_fs2.writeFileSync)(this.tokenPath, JSON.stringify(Object.fromEntries(this.tokens), null, 2));
}
getToken(serviceName) {
return this.tokens.get(serviceName);
}
setToken(serviceName, token) {
this.tokens.set(serviceName, token);
this.writeTokens();
}
generateToken() {
const randomBytes2 = crypto.randomBytes(16);
const bigInt = BigInt("0x" + randomBytes2.toString("hex")) + BigInt(Math.floor(Math.random() * 2 ** 14));
return bigInt.toString(32);
}
};
// src/server/server.ts
var import_crypto = require("crypto");
var defaultOptions = {
tokenPath: "tokens.json",
port: 8192
};
var VotifierServer = class extends import_events.EventEmitter {
server = (0, import_net.createServer)();
options;
protocolVersion = 2;
tokenManager;
constructor(options) {
super();
this.options = { ...defaultOptions, ...options };
this.tokenManager = new TokenManager(this.options.tokenPath);
this.server.on("connection", this.handleConnection.bind(this));
this.server.on("error", this.emit.bind(this, "error"));
}
handleConnection(socket) {
const challenge = this.tokenManager.generateToken();
const greeting = `VOTIFIER ${this.protocolVersion} ${challenge}
`;
socket.write(greeting);
const handleError = (error) => {
const errorResponse = {
status: "error",
error: error.message,
cause: error.name
};
socket.write(JSON.stringify(errorResponse));
socket.destroy();
socket.removeAllListeners();
this.emit("error", error);
};
const handleData = (data) => {
let payload;
try {
payload = this.decode(data, challenge);
} catch (error) {
return handleError(error);
}
const response = { status: "ok" };
socket.write(JSON.stringify(response));
socket.end();
socket.destroy();
this.emit("vote", payload);
};
socket.once("data", handleData);
socket.on("error", handleError);
}
decode(data, challenge) {
const magic = data.readUInt16BE(0);
if (magic !== magicV2) {
throw new Error("This server only accepts well-formed Votifier v2 packets.");
}
const messageLength = data.readUInt32BE(2);
const serializedMessage = data.subarray(4, 4 + messageLength).toString();
const message = JSON.parse(serializedMessage);
const payload = JSON.parse(message.payload);
if (payload.challenge !== challenge) {
throw new Error("Challenge is not valid");
}
let token = this.tokenManager.getToken(payload.serviceName);
if (!token) {
token = this.tokenManager.getToken("default");
if (!token) {
throw new Error(`Unknown service '${payload.serviceName}'`);
}
}
const serverSignature = (0, import_crypto.createHmac)("sha256", token).update(message.payload).digest("base64");
if (message.signature !== serverSignature) {
throw new Error("Signature is not valid (invalid token?)");
}
return payload;
}
start() {
this.server.listen(this.options.port, () => {
this.emit("listen");
});
}
stop() {
this.server.close();
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
TokenManager,
VotifierClient,
VotifierServer,
magicV2
});
//# sourceMappingURL=index.js.map
{"version":3,"sources":["../src/index.ts","../src/client/client.ts","../src/constants.ts","../src/server/server.ts","../src/server/tokens.ts"],"sourcesContent":["export * from './client/client';\nexport * from './constants';\nexport * from './server';\nexport * from './types';\n","import { readFileSync } from 'node:fs';\nimport { createConnection } from 'node:net';\nimport { createHmac } from 'node:crypto';\nimport type { VoteOptions, VotifierMessage, VotifierPayload, VotifierResponse } from '../types';\nimport { magicV2 } from '../constants';\n\nexport interface VotifierClientOptions {\n token: string;\n serviceName: string;\n host: string;\n /** Defaults to 8192 */\n port?: number;\n protocol?: 2;\n debug?: boolean;\n}\n\nexport class VotifierClient {\n public readonly host: string;\n\n public readonly port: number = 8192;\n\n public readonly token: string;\n \n public readonly protocolVersion: number = 2;\n \n public serviceName: string;\n\n public readonly debug: boolean = false;\n\n constructor(options: VotifierClientOptions) {\n this.token = options.token;\n this.serviceName = options.serviceName;\n this.host = options.host;\n if (options.port) this.port = options.port;\n if (options.protocol) this.protocolVersion = options.protocol;\n if (options.debug) this.debug = options.debug;\n }\n\n private getToken(path?: string): string {\n if (!path) throw new Error('Token or token path is required');\n return readFileSync(path, 'utf-8');\n } \n \n public sendVote(vote: VoteOptions) {\n return new Promise<void>((resolve, reject) => {\n const socket = createConnection(this.port, this.host);\n socket.setTimeout(5000, () => {\n socket.destroy();\n handleError(new Error('Connection timeout'));\n });\n\n const handleError = (error: Error) => {\n socket.destroy();\n socket.removeAllListeners();\n reject(error);\n }\n\n const handleGreeting = (data: Buffer) => {\n const greeting = data.toString();\n if (this.debug) console.log('[S->C] Greeting:', greeting);\n\n const headers = greeting.split(' ');\n if (headers.length !== 3) {\n return handleError(new Error('Not a v2 protocol server'));\n }\n\n const payload: VotifierPayload = {\n username: vote.username,\n address: vote.address,\n timestamp: vote.timestamp ?? Date.now(),\n serviceName: vote.serviceName ?? this.serviceName,\n challenge: headers[2]!.trim(),\n additionalData: (typeof vote.additionalData === 'string' ? Buffer.from(vote.additionalData) : vote.additionalData)?.toString('base64'),\n };\n if (this.debug) console.log('[C->S] Payload:', payload, payload.challenge.length);\n const serializedPayload = JSON.stringify(payload);\n\n const signature = createHmac('sha256', this.token)\n .update(serializedPayload)\n .digest('base64');\n\n const message: VotifierMessage = {\n payload: serializedPayload,\n signature\n };\n const serializedMessage = JSON.stringify(message);\n\n const buffer = Buffer.alloc(4 + serializedMessage.length);\n buffer.writeUInt16BE(magicV2, 0);\n buffer.writeUInt16BE(serializedMessage.length, 2);\n buffer.write(serializedMessage, 4);\n\n socket.write(new Uint8Array(buffer));\n socket.once('data', handleResponse);\n }\n\n const handleResponse = (data: Buffer) => {\n let response: VotifierResponse;\n try {\n response = JSON.parse(data.toString());\n } catch (error) {\n throw new Error('Failed to parse response');\n }\n\n if (this.debug) console.log('[S->C] Response:', response);\n\n socket.end();\n socket.removeAllListeners();\n\n if (response.status === 'ok') {\n resolve();\n\n } else if (response.status === 'error') {\n const err = new Error(response.error);\n err.name = response.cause;\n handleError(err);\n\n } else {\n handleError(new Error('Unknown response'));\n }\n }\n\n socket.once('data', handleGreeting);\n socket.once('error', handleError);\n });\n }\n}\n\n// export function sendVote() {}.\n/*\nfunc (client *V2Client) SendVote(vote Vote) error {\n\tconn, err := dial(client.address)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer conn.Close()\n\n\tgreeting := make([]byte, 64)\n\tread, err := conn.Read(greeting)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading greeting: %w\", err)\n\t}\n\n\tparts := bytes.Split(greeting[:read-1], []byte(\" \"))\n\tif len(parts) != 3 {\n\t\treturn errors.New(\"not a v2 server\")\n\t}\n\tchallenge := string(parts[2])\n\n\tserialized, err := vote.EncodeV2(client.token, challenge)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error encoding vote: %w\", err)\n\t}\n\t_, err = conn.Write(serialized)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to send vote: %w\", err)\n\t}\n\n\t// read response\n\tresBuf := make([]byte, 256)\n\tread, err = conn.Read(resBuf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading response: %w\", err)\n\t}\n\n\tvar res v2Response\n\trd := bytes.NewReader(resBuf[:read])\n\terr = json.NewDecoder(rd).Decode(&res)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error decoding response: %w\", err)\n\t}\n\n\tif !strings.EqualFold(res.Status, \"ok\") {\n\t\treturn fmt.Errorf(\"remote server error: %w\", &remoteError{\n\t\t\tcause: res.Cause,\n\t\t\terr: errors.New(res.Error),\n\t\t})\n\t}\n\n\treturn nil\n}\n\ntype remoteError struct {\n\tcause string\n\terr error\n}\n\nfunc (e *remoteError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", e.cause, e.err)\n}\n\nfunc (e *remoteError) Unwrap() error {\n\treturn e.err\n}\n*/","export const magicV2 = 0x733A;","import { EventEmitter } from 'events';\nimport { createServer, Server, Socket } from 'net';\nimport { TokenManager } from './tokens';\nimport { magicV2 } from '../constants';\nimport type { VotifierMessage, VotifierPayload, VotifierResponse } from '../types';\nimport { createHmac } from 'crypto';\n\nexport interface VotifierServerOptions {\n /** Path of the token file. Defaults to 'tokens.json' */\n tokenPath?: string;\n\n /** Port to listen vote requests. Defaults to 8192 */\n port?: number;\n}\n\nconst defaultOptions: VotifierServerOptions = {\n tokenPath: 'tokens.json',\n port: 8192,\n}\n\nexport class VotifierServer extends EventEmitter {\n public readonly server: Server = createServer();\n\n public readonly options: VotifierServerOptions;\n\n public readonly protocolVersion: number = 2;\n\n public readonly tokenManager: TokenManager;\n \n constructor(options?: VotifierServerOptions) {\n super();\n this.options = { ...defaultOptions, ...options };\n this.tokenManager = new TokenManager(this.options.tokenPath!);\n\n this.server.on('connection', this.handleConnection.bind(this));\n this.server.on('error', this.emit.bind(this, 'error'));\n }\n\n private handleConnection(socket: Socket): void {\n const challenge = this.tokenManager.generateToken();\n const greeting = `VOTIFIER ${this.protocolVersion} ${challenge}\\n`;\n socket.write(greeting);\n\n const handleError = (error: Error) => {\n const errorResponse: VotifierResponse = {\n status: 'error',\n error: error.message,\n cause: error.name,\n }\n socket.write(JSON.stringify(errorResponse));\n socket.destroy();\n socket.removeAllListeners();\n this.emit('error', error);\n }\n\n const handleData = (data: Buffer) => {\n let payload: VotifierPayload;\n try {\n payload = this.decode(data, challenge);\n } catch (error: any) {\n return handleError(error);\n }\n\n const response: VotifierResponse = { status: 'ok' };\n socket.write(JSON.stringify(response));\n socket.end();\n socket.destroy();\n\n this.emit('vote', payload);\n }\n \n socket.once('data', handleData);\n socket.on('error', handleError);\n }\n\n private decode(data: Buffer, challenge: string): VotifierPayload {\n const magic = data.readUInt16BE(0);\n if (magic !== magicV2) {\n throw new Error('This server only accepts well-formed Votifier v2 packets.');\n }\n\n const messageLength = data.readUInt32BE(2);\n const serializedMessage = data.subarray(4, 4 + messageLength).toString();\n \n const message: VotifierMessage = JSON.parse(serializedMessage);\n const payload: VotifierPayload = JSON.parse(message.payload);\n\n // verify challenge\n if (payload.challenge !== challenge) {\n throw new Error('Challenge is not valid');\n }\n\n let token = this.tokenManager.getToken(payload.serviceName);\n if (!token) {\n token = this.tokenManager.getToken('default');\n if (!token) {\n throw new Error(`Unknown service '${payload.serviceName}'`);\n }\n }\n\n // verify signature, decode\n const serverSignature = createHmac('sha256', token)\n .update(message.payload)\n .digest('base64');\n \n if (message.signature !== serverSignature) {\n throw new Error('Signature is not valid (invalid token?)');\n }\n\n return payload;\n }\n\n public start(): void {\n this.server.listen(this.options.port, () => {\n this.emit('listen');\n });\n }\n\n public stop(): void {\n this.server.close();\n }\n}\n\nexport interface VotifierServer {\n on(event: 'listen', listener: () => void): this;\n on(event: 'vote', listener: (vote: VotifierPayload) => void): this;\n on(event: 'error', listener: (error: any) => void): this;\n}\n","import * as crypto from 'crypto';\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport class TokenManager { \n private readonly tokenPath: string;\n\n private tokens: Map<string, string> = new Map();\n \n constructor(tokenPath: string) {\n this.tokenPath = tokenPath;\n this.load();\n }\n\n private load(): void {\n if (existsSync(this.tokenPath)) {\n const file = readFileSync(this.tokenPath, 'utf-8');\n this.tokens = new Map(Object.entries(JSON.parse(file)));\n } else {\n const defaultToken = this.generateToken();\n this.tokens.set('default', defaultToken);\n this.writeTokens();\n\n console.log('-'.repeat(75));\n console.log('[VotifierX]');\n console.log('No tokens were found in your tokenPath, so we\\'ve generated one for you.');\n console.log('Your default Votifier token is ' + defaultToken);\n console.log('You will need to provide this token when you submit your server to a voting');\n console.log('list.');\n console.log('-'.repeat(75));\n }\n }\n\n private writeTokens() {\n writeFileSync(this.tokenPath, JSON.stringify(Object.fromEntries(this.tokens), null, 2));\n }\n\n public getToken(serviceName: string): string | undefined {\n return this.tokens.get(serviceName);\n }\n\n public setToken(serviceName: string, token: string): void {\n this.tokens.set(serviceName, token);\n this.writeTokens();\n }\n \n public generateToken(): string {\n const randomBytes = crypto.randomBytes(16); // 128 bit\n const bigInt = BigInt('0x' + randomBytes.toString('hex')) + BigInt(Math.floor(Math.random() * 2 ** 14)); // 130ビットにするために2^14を足す\n // 32進数に変換\n return bigInt.toString(32);\n }\n}"],"mappings":"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","names":["import_node_fs","randomBytes"]}
// src/client/client.ts
import { readFileSync } from "node:fs";
import { createConnection } from "node:net";
import { createHmac } from "node:crypto";
// src/constants.ts
var magicV2 = 29498;
// src/client/client.ts
var VotifierClient = class {
host;
port = 8192;
token;
protocolVersion = 2;
serviceName;
debug = false;
constructor(options) {
this.token = options.token;
this.serviceName = options.serviceName;
this.host = options.host;
if (options.port) this.port = options.port;
if (options.protocol) this.protocolVersion = options.protocol;
if (options.debug) this.debug = options.debug;
}
getToken(path) {
if (!path) throw new Error("Token or token path is required");
return readFileSync(path, "utf-8");
}
sendVote(vote) {
return new Promise((resolve, reject) => {
const socket = createConnection(this.port, this.host);
socket.setTimeout(5e3, () => {
socket.destroy();
handleError(new Error("Connection timeout"));
});
const handleError = (error) => {
socket.destroy();
socket.removeAllListeners();
reject(error);
};
const handleGreeting = (data) => {
const greeting = data.toString();
if (this.debug) console.log("[S->C] Greeting:", greeting);
const headers = greeting.split(" ");
if (headers.length !== 3) {
return handleError(new Error("Not a v2 protocol server"));
}
const payload = {
username: vote.username,
address: vote.address,
timestamp: vote.timestamp ?? Date.now(),
serviceName: vote.serviceName ?? this.serviceName,
challenge: headers[2].trim(),
additionalData: (typeof vote.additionalData === "string" ? Buffer.from(vote.additionalData) : vote.additionalData)?.toString("base64")
};
if (this.debug) console.log("[C->S] Payload:", payload, payload.challenge.length);
const serializedPayload = JSON.stringify(payload);
const signature = createHmac("sha256", this.token).update(serializedPayload).digest("base64");
const message = {
payload: serializedPayload,
signature
};
const serializedMessage = JSON.stringify(message);
const buffer = Buffer.alloc(4 + serializedMessage.length);
buffer.writeUInt16BE(magicV2, 0);
buffer.writeUInt16BE(serializedMessage.length, 2);
buffer.write(serializedMessage, 4);
socket.write(new Uint8Array(buffer));
socket.once("data", handleResponse);
};
const handleResponse = (data) => {
let response;
try {
response = JSON.parse(data.toString());
} catch (error) {
throw new Error("Failed to parse response");
}
if (this.debug) console.log("[S->C] Response:", response);
socket.end();
socket.removeAllListeners();
if (response.status === "ok") {
resolve();
} else if (response.status === "error") {
const err = new Error(response.error);
err.name = response.cause;
handleError(err);
} else {
handleError(new Error("Unknown response"));
}
};
socket.once("data", handleGreeting);
socket.once("error", handleError);
});
}
};
// src/server/server.ts
import { EventEmitter } from "events";
import { createServer } from "net";
// src/server/tokens.ts
import * as crypto from "crypto";
import { existsSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
var TokenManager = class {
tokenPath;
tokens = /* @__PURE__ */ new Map();
constructor(tokenPath) {
this.tokenPath = tokenPath;
this.load();
}
load() {
if (existsSync(this.tokenPath)) {
const file = readFileSync2(this.tokenPath, "utf-8");
this.tokens = new Map(Object.entries(JSON.parse(file)));
} else {
const defaultToken = this.generateToken();
this.tokens.set("default", defaultToken);
this.writeTokens();
console.log("-".repeat(75));
console.log("[VotifierX]");
console.log("No tokens were found in your tokenPath, so we've generated one for you.");
console.log("Your default Votifier token is " + defaultToken);
console.log("You will need to provide this token when you submit your server to a voting");
console.log("list.");
console.log("-".repeat(75));
}
}
writeTokens() {
writeFileSync(this.tokenPath, JSON.stringify(Object.fromEntries(this.tokens), null, 2));
}
getToken(serviceName) {
return this.tokens.get(serviceName);
}
setToken(serviceName, token) {
this.tokens.set(serviceName, token);
this.writeTokens();
}
generateToken() {
const randomBytes2 = crypto.randomBytes(16);
const bigInt = BigInt("0x" + randomBytes2.toString("hex")) + BigInt(Math.floor(Math.random() * 2 ** 14));
return bigInt.toString(32);
}
};
// src/server/server.ts
import { createHmac as createHmac2 } from "crypto";
var defaultOptions = {
tokenPath: "tokens.json",
port: 8192
};
var VotifierServer = class extends EventEmitter {
server = createServer();
options;
protocolVersion = 2;
tokenManager;
constructor(options) {
super();
this.options = { ...defaultOptions, ...options };
this.tokenManager = new TokenManager(this.options.tokenPath);
this.server.on("connection", this.handleConnection.bind(this));
this.server.on("error", this.emit.bind(this, "error"));
}
handleConnection(socket) {
const challenge = this.tokenManager.generateToken();
const greeting = `VOTIFIER ${this.protocolVersion} ${challenge}
`;
socket.write(greeting);
const handleError = (error) => {
const errorResponse = {
status: "error",
error: error.message,
cause: error.name
};
socket.write(JSON.stringify(errorResponse));
socket.destroy();
socket.removeAllListeners();
this.emit("error", error);
};
const handleData = (data) => {
let payload;
try {
payload = this.decode(data, challenge);
} catch (error) {
return handleError(error);
}
const response = { status: "ok" };
socket.write(JSON.stringify(response));
socket.end();
socket.destroy();
this.emit("vote", payload);
};
socket.once("data", handleData);
socket.on("error", handleError);
}
decode(data, challenge) {
const magic = data.readUInt16BE(0);
if (magic !== magicV2) {
throw new Error("This server only accepts well-formed Votifier v2 packets.");
}
const messageLength = data.readUInt32BE(2);
const serializedMessage = data.subarray(4, 4 + messageLength).toString();
const message = JSON.parse(serializedMessage);
const payload = JSON.parse(message.payload);
if (payload.challenge !== challenge) {
throw new Error("Challenge is not valid");
}
let token = this.tokenManager.getToken(payload.serviceName);
if (!token) {
token = this.tokenManager.getToken("default");
if (!token) {
throw new Error(`Unknown service '${payload.serviceName}'`);
}
}
const serverSignature = createHmac2("sha256", token).update(message.payload).digest("base64");
if (message.signature !== serverSignature) {
throw new Error("Signature is not valid (invalid token?)");
}
return payload;
}
start() {
this.server.listen(this.options.port, () => {
this.emit("listen");
});
}
stop() {
this.server.close();
}
};
export {
TokenManager,
VotifierClient,
VotifierServer,
magicV2
};
//# sourceMappingURL=index.mjs.map
{"version":3,"sources":["../src/client/client.ts","../src/constants.ts","../src/server/server.ts","../src/server/tokens.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { createConnection } from 'node:net';\nimport { createHmac } from 'node:crypto';\nimport type { VoteOptions, VotifierMessage, VotifierPayload, VotifierResponse } from '../types';\nimport { magicV2 } from '../constants';\n\nexport interface VotifierClientOptions {\n token: string;\n serviceName: string;\n host: string;\n /** Defaults to 8192 */\n port?: number;\n protocol?: 2;\n debug?: boolean;\n}\n\nexport class VotifierClient {\n public readonly host: string;\n\n public readonly port: number = 8192;\n\n public readonly token: string;\n \n public readonly protocolVersion: number = 2;\n \n public serviceName: string;\n\n public readonly debug: boolean = false;\n\n constructor(options: VotifierClientOptions) {\n this.token = options.token;\n this.serviceName = options.serviceName;\n this.host = options.host;\n if (options.port) this.port = options.port;\n if (options.protocol) this.protocolVersion = options.protocol;\n if (options.debug) this.debug = options.debug;\n }\n\n private getToken(path?: string): string {\n if (!path) throw new Error('Token or token path is required');\n return readFileSync(path, 'utf-8');\n } \n \n public sendVote(vote: VoteOptions) {\n return new Promise<void>((resolve, reject) => {\n const socket = createConnection(this.port, this.host);\n socket.setTimeout(5000, () => {\n socket.destroy();\n handleError(new Error('Connection timeout'));\n });\n\n const handleError = (error: Error) => {\n socket.destroy();\n socket.removeAllListeners();\n reject(error);\n }\n\n const handleGreeting = (data: Buffer) => {\n const greeting = data.toString();\n if (this.debug) console.log('[S->C] Greeting:', greeting);\n\n const headers = greeting.split(' ');\n if (headers.length !== 3) {\n return handleError(new Error('Not a v2 protocol server'));\n }\n\n const payload: VotifierPayload = {\n username: vote.username,\n address: vote.address,\n timestamp: vote.timestamp ?? Date.now(),\n serviceName: vote.serviceName ?? this.serviceName,\n challenge: headers[2]!.trim(),\n additionalData: (typeof vote.additionalData === 'string' ? Buffer.from(vote.additionalData) : vote.additionalData)?.toString('base64'),\n };\n if (this.debug) console.log('[C->S] Payload:', payload, payload.challenge.length);\n const serializedPayload = JSON.stringify(payload);\n\n const signature = createHmac('sha256', this.token)\n .update(serializedPayload)\n .digest('base64');\n\n const message: VotifierMessage = {\n payload: serializedPayload,\n signature\n };\n const serializedMessage = JSON.stringify(message);\n\n const buffer = Buffer.alloc(4 + serializedMessage.length);\n buffer.writeUInt16BE(magicV2, 0);\n buffer.writeUInt16BE(serializedMessage.length, 2);\n buffer.write(serializedMessage, 4);\n\n socket.write(new Uint8Array(buffer));\n socket.once('data', handleResponse);\n }\n\n const handleResponse = (data: Buffer) => {\n let response: VotifierResponse;\n try {\n response = JSON.parse(data.toString());\n } catch (error) {\n throw new Error('Failed to parse response');\n }\n\n if (this.debug) console.log('[S->C] Response:', response);\n\n socket.end();\n socket.removeAllListeners();\n\n if (response.status === 'ok') {\n resolve();\n\n } else if (response.status === 'error') {\n const err = new Error(response.error);\n err.name = response.cause;\n handleError(err);\n\n } else {\n handleError(new Error('Unknown response'));\n }\n }\n\n socket.once('data', handleGreeting);\n socket.once('error', handleError);\n });\n }\n}\n\n// export function sendVote() {}.\n/*\nfunc (client *V2Client) SendVote(vote Vote) error {\n\tconn, err := dial(client.address)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer conn.Close()\n\n\tgreeting := make([]byte, 64)\n\tread, err := conn.Read(greeting)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading greeting: %w\", err)\n\t}\n\n\tparts := bytes.Split(greeting[:read-1], []byte(\" \"))\n\tif len(parts) != 3 {\n\t\treturn errors.New(\"not a v2 server\")\n\t}\n\tchallenge := string(parts[2])\n\n\tserialized, err := vote.EncodeV2(client.token, challenge)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error encoding vote: %w\", err)\n\t}\n\t_, err = conn.Write(serialized)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to send vote: %w\", err)\n\t}\n\n\t// read response\n\tresBuf := make([]byte, 256)\n\tread, err = conn.Read(resBuf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading response: %w\", err)\n\t}\n\n\tvar res v2Response\n\trd := bytes.NewReader(resBuf[:read])\n\terr = json.NewDecoder(rd).Decode(&res)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error decoding response: %w\", err)\n\t}\n\n\tif !strings.EqualFold(res.Status, \"ok\") {\n\t\treturn fmt.Errorf(\"remote server error: %w\", &remoteError{\n\t\t\tcause: res.Cause,\n\t\t\terr: errors.New(res.Error),\n\t\t})\n\t}\n\n\treturn nil\n}\n\ntype remoteError struct {\n\tcause string\n\terr error\n}\n\nfunc (e *remoteError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", e.cause, e.err)\n}\n\nfunc (e *remoteError) Unwrap() error {\n\treturn e.err\n}\n*/","export const magicV2 = 0x733A;","import { EventEmitter } from 'events';\nimport { createServer, Server, Socket } from 'net';\nimport { TokenManager } from './tokens';\nimport { magicV2 } from '../constants';\nimport type { VotifierMessage, VotifierPayload, VotifierResponse } from '../types';\nimport { createHmac } from 'crypto';\n\nexport interface VotifierServerOptions {\n /** Path of the token file. Defaults to 'tokens.json' */\n tokenPath?: string;\n\n /** Port to listen vote requests. Defaults to 8192 */\n port?: number;\n}\n\nconst defaultOptions: VotifierServerOptions = {\n tokenPath: 'tokens.json',\n port: 8192,\n}\n\nexport class VotifierServer extends EventEmitter {\n public readonly server: Server = createServer();\n\n public readonly options: VotifierServerOptions;\n\n public readonly protocolVersion: number = 2;\n\n public readonly tokenManager: TokenManager;\n \n constructor(options?: VotifierServerOptions) {\n super();\n this.options = { ...defaultOptions, ...options };\n this.tokenManager = new TokenManager(this.options.tokenPath!);\n\n this.server.on('connection', this.handleConnection.bind(this));\n this.server.on('error', this.emit.bind(this, 'error'));\n }\n\n private handleConnection(socket: Socket): void {\n const challenge = this.tokenManager.generateToken();\n const greeting = `VOTIFIER ${this.protocolVersion} ${challenge}\\n`;\n socket.write(greeting);\n\n const handleError = (error: Error) => {\n const errorResponse: VotifierResponse = {\n status: 'error',\n error: error.message,\n cause: error.name,\n }\n socket.write(JSON.stringify(errorResponse));\n socket.destroy();\n socket.removeAllListeners();\n this.emit('error', error);\n }\n\n const handleData = (data: Buffer) => {\n let payload: VotifierPayload;\n try {\n payload = this.decode(data, challenge);\n } catch (error: any) {\n return handleError(error);\n }\n\n const response: VotifierResponse = { status: 'ok' };\n socket.write(JSON.stringify(response));\n socket.end();\n socket.destroy();\n\n this.emit('vote', payload);\n }\n \n socket.once('data', handleData);\n socket.on('error', handleError);\n }\n\n private decode(data: Buffer, challenge: string): VotifierPayload {\n const magic = data.readUInt16BE(0);\n if (magic !== magicV2) {\n throw new Error('This server only accepts well-formed Votifier v2 packets.');\n }\n\n const messageLength = data.readUInt32BE(2);\n const serializedMessage = data.subarray(4, 4 + messageLength).toString();\n \n const message: VotifierMessage = JSON.parse(serializedMessage);\n const payload: VotifierPayload = JSON.parse(message.payload);\n\n // verify challenge\n if (payload.challenge !== challenge) {\n throw new Error('Challenge is not valid');\n }\n\n let token = this.tokenManager.getToken(payload.serviceName);\n if (!token) {\n token = this.tokenManager.getToken('default');\n if (!token) {\n throw new Error(`Unknown service '${payload.serviceName}'`);\n }\n }\n\n // verify signature, decode\n const serverSignature = createHmac('sha256', token)\n .update(message.payload)\n .digest('base64');\n \n if (message.signature !== serverSignature) {\n throw new Error('Signature is not valid (invalid token?)');\n }\n\n return payload;\n }\n\n public start(): void {\n this.server.listen(this.options.port, () => {\n this.emit('listen');\n });\n }\n\n public stop(): void {\n this.server.close();\n }\n}\n\nexport interface VotifierServer {\n on(event: 'listen', listener: () => void): this;\n on(event: 'vote', listener: (vote: VotifierPayload) => void): this;\n on(event: 'error', listener: (error: any) => void): this;\n}\n","import * as crypto from 'crypto';\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport class TokenManager { \n private readonly tokenPath: string;\n\n private tokens: Map<string, string> = new Map();\n \n constructor(tokenPath: string) {\n this.tokenPath = tokenPath;\n this.load();\n }\n\n private load(): void {\n if (existsSync(this.tokenPath)) {\n const file = readFileSync(this.tokenPath, 'utf-8');\n this.tokens = new Map(Object.entries(JSON.parse(file)));\n } else {\n const defaultToken = this.generateToken();\n this.tokens.set('default', defaultToken);\n this.writeTokens();\n\n console.log('-'.repeat(75));\n console.log('[VotifierX]');\n console.log('No tokens were found in your tokenPath, so we\\'ve generated one for you.');\n console.log('Your default Votifier token is ' + defaultToken);\n console.log('You will need to provide this token when you submit your server to a voting');\n console.log('list.');\n console.log('-'.repeat(75));\n }\n }\n\n private writeTokens() {\n writeFileSync(this.tokenPath, JSON.stringify(Object.fromEntries(this.tokens), null, 2));\n }\n\n public getToken(serviceName: string): string | undefined {\n return this.tokens.get(serviceName);\n }\n\n public setToken(serviceName: string, token: string): void {\n this.tokens.set(serviceName, token);\n this.writeTokens();\n }\n \n public generateToken(): string {\n const randomBytes = crypto.randomBytes(16); // 128 bit\n const bigInt = BigInt('0x' + randomBytes.toString('hex')) + BigInt(Math.floor(Math.random() * 2 ** 14)); // 130ビットにするために2^14を足す\n // 32進数に変換\n return bigInt.toString(32);\n }\n}"],"mappings":"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","names":["readFileSync","readFileSync","randomBytes","createHmac"]}
MIT License
Copyright (c) 2025 SKYNETWORK-MCBE
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+14
-4
{
"name": "votifier-x",
"version": "0.1.0",
"version": "0.1.1",
"description": "Votifier implementation in TypeScript. Including standalone server and client.",

@@ -19,4 +19,4 @@ "exports": {

"scripts": {
"dev-server": "bun test/server.js",
"dev-client": "bun test/client.js",
"dev-server": "bun test/server.mjs",
"dev-client": "bun test/client.mjs",
"build": "tsup",

@@ -33,3 +33,13 @@ "lint": "tsc --noEmit"

"dist"
]
],
"repository": {
"type": "git",
"url": "https://github.com/SKYNETWORK-MCBE/VotifierX.git"
},
"bugs": {
"url": "https://github.com/SKYNETWORK-MCBE/VotifierX/issues"
},
"engines": {
"node": ">=16.0.0"
}
}