stuff and junk and things

This commit is contained in:
Kim Ravn Hansen
2025-09-05 17:57:09 +02:00
parent 8bcbdbfe35
commit 1720db9eb7
14 changed files with 460 additions and 333 deletions

View File

@@ -1,3 +1,4 @@
# Ignore artifacts: # Ignore artifacts:
build build
coverage coverage
node_modules

View File

@@ -1 +1,3 @@
{} {
"tabWidth": 4
}

View File

@@ -7,25 +7,32 @@ import * as id from "../utils/id.js";
* @class * @class
*/ */
export class Character { export class Character {
/** @type {string} character's name */ /** @type {string} character's name */
name; name;
/** @protected @type {number} The number of XP the character has. */ /** @protected @type {number} The number of XP the character has. */
_xp = 0; _xp = 0;
get xp() { return this._xp; } get xp() {
return this._xp;
}
/** @protected @type {number} The character's level. */ /** @protected @type {number} The character's level. */
_level = 1; _level = 1;
get level() { return this._level; } get level() {
return this._level;
}
/** @protected @type {string} unique name used for chats when there's a name clash and also other things that require a unique character id */ /** @protected @type {string} unique name used for chats when there's a name clash and also other things that require a unique character id */
_id; _id;
get id() { return this._id; } get id() {
return this._id;
}
/** @protected @type {string} username of the player that owns this character. */ /** @protected @type {string} username of the player that owns this character. */
_username; _username;
get username() { return this._username; } get username() {
return this._username;
}
/** @type {string} Bloodline background */ /** @type {string} Bloodline background */
ancestry; ancestry;
@@ -57,7 +64,6 @@ export class Character {
* @param {boolean} initialize Should we initialize the character * @param {boolean} initialize Should we initialize the character
*/ */
constructor(playerUname, name, initialize) { constructor(playerUname, name, initialize) {
this.name = name; this.name = name;
// Initialize the unique name if this character. // Initialize the unique name if this character.
@@ -138,7 +144,9 @@ export class Character {
this.meleeCombat = Math.max(this.skulduggery, 10); this.meleeCombat = Math.max(this.skulduggery, 10);
break; break;
default: default:
throw new Error('Logic error, ancestry d8() roll was out of scope'); throw new Error(
"Logic error, ancestry d8() roll was out of scope",
);
} }
// //
@@ -167,7 +175,7 @@ export class Character {
this.equipment this.equipment
.set("sickle", 1) .set("sickle", 1)
.set("poisoner's kit", 1) .set("poisoner's kit", 1)
.set("healer's kit", 1) .set("healer's kit", 1);
default: default:
this.foundation = "debug"; this.foundation = "debug";
this.proficiencies.add("heavy_armor"); this.proficiencies.add("heavy_armor");

View File

@@ -12,7 +12,6 @@ import { Character } from "./character.js";
import { ItemTemplate } from "./item.js"; import { ItemTemplate } from "./item.js";
export class Game { export class Game {
/** @type {Map<string,ItemTemplate>} List of all item templates in the game */ /** @type {Map<string,ItemTemplate>} List of all item templates in the game */
_itemTemplates = new Map(); _itemTemplates = new Map();
@@ -33,5 +32,8 @@ export class Game{
* @protected * @protected
* @type {Map<string,Player>} Map of users in the game username->Player * @type {Map<string,Player>} Map of users in the game username->Player
*/ */
_players = new Map(); get players() { return this._players; } _players = new Map();
get players() {
return this._players;
}
} }

View File

@@ -7,7 +7,6 @@ import { cleanName } from "../utils/id.js";
* generate these CharacterItems. * generate these CharacterItems.
*/ */
export class ItemTemplate { export class ItemTemplate {
_id; _id;
_name; _name;
_description; _description;
@@ -42,15 +41,18 @@ export class ItemTemplate {
* @param {string=} id Item's machine-friendly name. * @param {string=} id Item's machine-friendly name.
*/ */
constructor(name, itemSlots, description, id) { constructor(name, itemSlots, description, id) {
if (typeof name !== "string") { if (typeof name !== "string") {
throw new Error("Name must be a string, but " + typeof name + " given."); throw new Error(
"Name must be a string, but " + typeof name + " given.",
);
} }
if (typeof description === "undefined") { if (typeof description === "undefined") {
description = ""; description = "";
} }
if (typeof description !== "string") { if (typeof description !== "string") {
throw new Error("Name must be a string, but " + typeof name + " given."); throw new Error(
"Name must be a string, but " + typeof name + " given.",
);
} }
if (!Number.isFinite(itemSlots)) { if (!Number.isFinite(itemSlots)) {
throw new Error("itemSlots must be a finite number!"); throw new Error("itemSlots must be a finite number!");
@@ -66,11 +68,15 @@ export class ItemTemplate {
this._id = id; this._id = id;
this._itemSlots = Number(itemSlots); this._itemSlots = Number(itemSlots);
this._description = ""; this._description = "";
} }
createItem() { createItem() {
return new ChracterItem(this._id, this._name, this._description, this._itemSlots); return new ChracterItem(
this._id,
this._name,
this._description,
this._itemSlots,
);
} }
} }
@@ -94,7 +100,6 @@ export class ItemTemplate {
* Another bonus is, that the game can spawn custom items that arent even in the ItemTemplate Set. * Another bonus is, that the game can spawn custom items that arent even in the ItemTemplate Set.
*/ */
export class CharacterItem { export class CharacterItem {
/** @type {string?} The unique name if the ItemTemplate this item is based on. May be null. */ /** @type {string?} The unique name if the ItemTemplate this item is based on. May be null. */
templateItemId; // We use the id instead of a pointer, could make garbage collection better. templateItemId; // We use the id instead of a pointer, could make garbage collection better.
@@ -119,7 +124,3 @@ const i = new ItemTemplate("knife", 10000);
const ci = new CharacterItem(); const ci = new CharacterItem();
console.log(ci); console.log(ci);

View File

@@ -1,4 +1,4 @@
import WebSocket from 'ws'; import WebSocket from "ws";
/** /**
* Player Account. * Player Account.
@@ -16,15 +16,21 @@ import WebSocket from 'ws';
export class Player { export class Player {
/** @protected @type {string} unique username */ /** @protected @type {string} unique username */
_username; _username;
get username() { return this._username; } get username() {
return this._username;
}
/** @protected @type {string} */ /** @protected @type {string} */
_passwordHash; _passwordHash;
get passwordHash() { return this._passwordHash; } get passwordHash() {
return this._passwordHash;
}
/** @protected @type {WebSocket} Player's current and only websocket. If undefined, the player is not logged in. */ /** @protected @type {WebSocket} Player's current and only websocket. If undefined, the player is not logged in. */
_websocket; _websocket;
get websocket() { return this._websocket; } get websocket() {
return this._websocket;
}
/** @protected @type {Date} */ /** @protected @type {Date} */
_latestSocketReceived; _latestSocketReceived;
@@ -46,7 +52,11 @@ export class Player{
*/ */
_send(data) { _send(data) {
if (!this._websocket) { if (!this._websocket) {
console.error("Trying to send a message to an uninitialized websocket", this, data) console.error(
"Trying to send a message to an uninitialized websocket",
this,
data,
);
return false; return false;
} }
if (this._websocket.readyState === WebSocket.OPEN) { if (this._websocket.readyState === WebSocket.OPEN) {
@@ -54,19 +64,36 @@ export class Player{
return true; return true;
} }
if (this._websocket.readyState === WebSocket.CLOSED) { if (this._websocket.readyState === WebSocket.CLOSED) {
console.error("Trying to send a message through a CLOSED websocket", this, data); console.error(
"Trying to send a message through a CLOSED websocket",
this,
data,
);
return false; return false;
} }
if (this._websocket.readyState === WebSocket.CLOSING) { if (this._websocket.readyState === WebSocket.CLOSING) {
console.error("Trying to send a message through a CLOSING websocket", this, data); console.error(
"Trying to send a message through a CLOSING websocket",
this,
data,
);
return false; return false;
} }
if (this._websocket.readyState === WebSocket.CONNECTING) { if (this._websocket.readyState === WebSocket.CONNECTING) {
console.error("Trying to send a message through a CONNECTING (not yet open) websocket", this, data); console.error(
"Trying to send a message through a CONNECTING (not yet open) websocket",
this,
data,
);
return false; return false;
} }
console.error("Trying to send a message through a websocket with an UNKNOWN readyState (%d)", this.websocket.readyState, this, data); console.error(
"Trying to send a message through a websocket with an UNKNOWN readyState (%d)",
this.websocket.readyState,
this,
data,
);
return false; return false;
} }
@@ -74,5 +101,3 @@ export class Player{
this.sendMessage(`\n[${this.currentRoom}] > `); this.sendMessage(`\n[${this.currentRoom}] > `);
} }
} }

View File

@@ -8,7 +8,6 @@
* @todo Add encounters to portals * @todo Add encounters to portals
*/ */
export class Portal { export class Portal {
/** /**
* Target Location. * Target Location.
*/ */
@@ -23,5 +22,4 @@ export class Portal {
* Description shown to the player when they traverse the portal. * Description shown to the player when they traverse the portal.
*/ */
_traversalDescription; _traversalDescription;
} }

View File

@@ -1,12 +1,12 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebSocket MUD</title> <title>WebSocket MUD</title>
<style> <style>
body { body {
font-family: 'Courier New', monospace; font-family: "Courier New", monospace;
background-color: #1a1a1a; background-color: #1a1a1a;
color: #00ff00; color: #00ff00;
margin: 0; margin: 0;
@@ -48,7 +48,7 @@
border: 2px solid #333; border: 2px solid #333;
color: #00ff00; color: #00ff00;
padding: 10px; padding: 10px;
font-family: 'Courier New', monospace; font-family: "Courier New", monospace;
font-size: 14px; font-size: 14px;
} }
@@ -62,7 +62,7 @@
border: 2px solid #555; border: 2px solid #555;
color: #00ff00; color: #00ff00;
padding: 10px 20px; padding: 10px 20px;
font-family: 'Courier New', monospace; font-family: "Courier New", monospace;
cursor: pointer; cursor: pointer;
} }
@@ -107,7 +107,12 @@
<div id="status" class="connecting">Connecting...</div> <div id="status" class="connecting">Connecting...</div>
<div id="output"></div> <div id="output"></div>
<div id="input-container"> <div id="input-container">
<input type="text" id="input" placeholder="Enter command..." disabled> <input
type="text"
id="input"
placeholder="Enter command..."
disabled
/>
<button id="send" disabled>Send</button> <button id="send" disabled>Send</button>
</div> </div>
</div> </div>
@@ -116,26 +121,27 @@
class MUDClient { class MUDClient {
constructor() { constructor() {
this.ws = null; this.ws = null;
this.output = document.getElementById('output'); this.output = document.getElementById("output");
this.input = document.getElementById('input'); this.input = document.getElementById("input");
this.sendButton = document.getElementById('send'); this.sendButton = document.getElementById("send");
this.status = document.getElementById('status'); this.status = document.getElementById("status");
this.setupEventListeners(); this.setupEventListeners();
this.connect(); this.connect();
} }
connect() { connect() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol =
window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}`; const wsUrl = `${protocol}//${window.location.host}`;
this.updateStatus('Connecting...', 'connecting'); this.updateStatus("Connecting...", "connecting");
try { try {
this.s = new WebSocket(wsUrl); this.s = new WebSocket(wsUrl);
this.ws.onopen = () => { this.ws.onopen = () => {
this.updateStatus('Connected', 'connected'); this.updateStatus("Connected", "connected");
this.input.disabled = false; this.input.disabled = false;
this.sendButton.disabled = false; this.sendButton.disabled = false;
this.input.focus(); this.input.focus();
@@ -147,7 +153,7 @@
}; };
this.ws.onclose = () => { this.ws.onclose = () => {
this.updateStatus('Disconnected', 'disconnected'); this.updateStatus("Disconnected", "disconnected");
this.input.disabled = true; this.input.disabled = true;
this.sendButton.disabled = true; this.sendButton.disabled = true;
@@ -156,24 +162,26 @@
}; };
this.ws.onerror = (error) => { this.ws.onerror = (error) => {
this.updateStatus('Connection Error', 'error'); this.updateStatus("Connection Error", "error");
this.appendOutput('Connection error occurred. Retrying...', 'error'); this.appendOutput(
"Connection error occurred. Retrying...",
"error",
);
}; };
} catch (error) { } catch (error) {
this.updateStatus('Connection Failed', 'error'); this.updateStatus("Connection Failed", "error");
setTimeout(() => this.connect(), 3000); setTimeout(() => this.connect(), 3000);
} }
} }
setupEventListeners() { setupEventListeners() {
this.input.addEventListener('keypress', (e) => { this.input.addEventListener("keypress", (e) => {
if (e.key === 'Enter') { if (e.key === "Enter") {
this.sendMessage(); this.sendMessage();
} }
}); });
this.sendButton.addEventListener('click', () => { this.sendButton.addEventListener("click", () => {
this.sendMessage(); this.sendMessage();
}); });
@@ -181,21 +189,34 @@
this.commandHistory = []; this.commandHistory = [];
this.historyIndex = -1; this.historyIndex = -1;
this.input.addEventListener('keydown', (e) => { this.input.addEventListener("keydown", (e) => {
if (e.key === 'ArrowUp') { if (e.key === "ArrowUp") {
e.preventDefault(); e.preventDefault();
if (this.historyIndex < this.commandHistory.length - 1) { if (
this.historyIndex <
this.commandHistory.length - 1
) {
this.historyIndex++; this.historyIndex++;
this.input.value = this.commandHistory[this.commandHistory.length - 1 - this.historyIndex]; this.input.value =
this.commandHistory[
this.commandHistory.length -
1 -
this.historyIndex
];
} }
} else if (e.key === 'ArrowDown') { } else if (e.key === "ArrowDown") {
e.preventDefault(); e.preventDefault();
if (this.historyIndex > 0) { if (this.historyIndex > 0) {
this.historyIndex--; this.historyIndex--;
this.input.value = this.commandHistory[this.commandHistory.length - 1 - this.historyIndex]; this.input.value =
this.commandHistory[
this.commandHistory.length -
1 -
this.historyIndex
];
} else if (this.historyIndex === 0) { } else if (this.historyIndex === 0) {
this.historyIndex = -1; this.historyIndex = -1;
this.input.value = ''; this.input.value = "";
} }
} }
}); });
@@ -203,9 +224,17 @@
sendMessage() { sendMessage() {
const message = this.input.value.trim(); const message = this.input.value.trim();
if (message && this.ws && this.ws.readyState === WebSocket.OPEN) { if (
message &&
this.ws &&
this.ws.readyState === WebSocket.OPEN
) {
// Add to command history // Add to command history
if (this.commandHistory[this.commandHistory.length - 1] !== message) { if (
this.commandHistory[
this.commandHistory.length - 1
] !== message
) {
this.commandHistory.push(message); this.commandHistory.push(message);
if (this.commandHistory.length > 50) { if (this.commandHistory.length > 50) {
this.commandHistory.shift(); this.commandHistory.shift();
@@ -213,40 +242,42 @@
} }
this.historyIndex = -1; this.historyIndex = -1;
this.ws.send(JSON.stringify({ this.ws.send(
type: 'command', JSON.stringify({
content: message type: "command",
})); content: message,
}),
);
this.input.value = ''; this.input.value = "";
} }
} }
handleMessage(data) { handleMessage(data) {
switch (data.type) { switch (data.type) {
case 'message': case "message":
this.appendOutput(data.content); this.appendOutput(data.content);
break; break;
case 'error': case "error":
this.appendOutput(data.content, 'error'); this.appendOutput(data.content, "error");
break; break;
case 'system': case "system":
this.appendOutput(data.content, 'system'); this.appendOutput(data.content, "system");
break; break;
default: default:
this.appendOutput(data.content); this.appendOutput(data.content);
} }
} }
appendOutput(text, className = '') { appendOutput(text, className = "") {
const div = document.createElement('div'); const div = document.createElement("div");
if (className) { if (className) {
div.className = className; div.className = className;
} }
// Check if this looks like a prompt // Check if this looks like a prompt
if (text.includes('] > ')) { if (text.includes("] > ")) {
div.className = 'prompt'; div.className = "prompt";
} }
div.textContent = text; div.textContent = text;
@@ -261,7 +292,7 @@
} }
// Initialize the MUD client when the page loads // Initialize the MUD client when the page loads
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
new MUDClient(); new MUDClient();
}); });
</script> </script>

View File

@@ -4,7 +4,7 @@ import path from "path";
import fs from "fs"; import fs from "fs";
import { Player } from "./models/player.js"; import { Player } from "./models/player.js";
import { Game } from "./models/game.js"; import { Game } from "./models/game.js";
import { ClientMessage, MSG_ERROR, MSG_MESSAGE, MSG_PROMPT, MSG_CALAMITY } from "./utils/messages.js"; import { ClientMessage, MSG_ERROR, MSG_MESSAGE, MSG_PROMPT, MSG_CALAMITY, } from "./utils/messages.js";
class Session { class Session {
/** @type {boolean} */ /** @type {boolean} */
@@ -49,8 +49,12 @@ class MudServer {
console.log("New connection established"); console.log("New connection established");
this.sessions[websocket] = new Session(); this.sessions[websocket] = new Session();
websocket.on("message", (data) => { this.onIncomingMessage(websocket, data) }); websocket.on("message", (data) => {
websocket.on("close", () => { this.onConnectionClosed(websocket); }); this.onIncomingMessage(websocket, data);
});
websocket.on("close", () => {
this.onConnectionClosed(websocket);
});
this.send(websocket, MSG_MESSAGE, "Welcome to MUUUHD", "big"); this.send(websocket, MSG_MESSAGE, "Welcome to MUUUHD", "big");
this.send(websocket, MSG_PROMPT, "Please enter your username"); this.send(websocket, MSG_PROMPT, "Please enter your username");
@@ -64,10 +68,18 @@ class MudServer {
const session = this.sessions.get(websocket); const session = this.sessions.get(websocket);
if (!session) { if (!session) {
console.error("Incoming message from a client without a session!", data); console.error(
this.send(websocket, MSG_ERROR, "terminal", "You do not have an active session. Go away!") "Incoming message from a client without a session!",
data,
);
this.send(
websocket,
MSG_ERROR,
"terminal",
"You do not have an active session. Go away!",
);
websocket.close(); websocket.close();
return return;
} }
let message; let message;
@@ -76,9 +88,14 @@ class MudServer {
message = new ClientMessage(data); message = new ClientMessage(data);
} catch (error) { } catch (error) {
console.error("Bad websocket message", data, error); console.error("Bad websocket message", data, error);
this.send(websocket, MSG_ERROR, "terminal", "You sent me a bad message! Goodbye...") this.send(
websocket,
MSG_ERROR,
"terminal",
"You sent me a bad message! Goodbye...",
);
websocket.close(); websocket.close();
return return;
} }
if (!session.usernameProcessed) { if (!session.usernameProcessed) {
@@ -87,8 +104,14 @@ class MudServer {
// We haven"t gotten a username yet, so we expect one. // We haven"t gotten a username yet, so we expect one.
//---------------------------------------------------- //----------------------------------------------------
if (!message.hasUsername()) { if (!message.hasUsername()) {
console.error("User should have sent a “username” message, but sent something else instead") console.error(
this.send(websocket, MSG_CALAMITY, "I expected you to send me a username, but you sent me something else instead. You bad! Goodbye...") "User should have sent a username” message, but sent something else instead",
);
this.send(
websocket,
MSG_CALAMITY,
"I expected you to send me a username, but you sent me something else instead. You bad! Goodbye...",
);
// for now, just close the socket. // for now, just close the socket.
websocket.close(); websocket.close();
@@ -100,7 +123,11 @@ class MudServer {
// player not found - for now, just close the connection - make a better // player not found - for now, just close the connection - make a better
console.log("Invalid username sent during login: %s", username); console.log("Invalid username sent during login: %s", username);
this.send(websocket, MSG_ERROR, "Invalid username"); this.send(websocket, MSG_ERROR, "Invalid username");
this.send(websocket, MSG_PROMPT, "Please enter a valid username"); this.send(
websocket,
MSG_PROMPT,
"Please enter a valid username",
);
} }
// correct username, tentatively assign player to session // correct username, tentatively assign player to session
@@ -120,11 +147,13 @@ class MudServer {
//---------------------------------------------------- //----------------------------------------------------
if (!session.passwordProcessed) { if (!session.passwordProcessed) {
if (!message.hasPassword) { if (!message.hasPassword) {
console.error("Youser should have sent a “password” message, but sent this instead: %s", message.type); console.error(
"Youser should have sent a “password” message, but sent this instead: %s",
message.type,
);
} }
} }
// //
//---------------------------------------------------- //----------------------------------------------------
// Process the player's commands // Process the player's commands
@@ -134,7 +163,10 @@ class MudServer {
return; return;
} }
console.error("We have received a message we couldn't handle!!!", message); console.error(
"We have received a message we couldn't handle!!!",
message,
);
} }
/** /**
@@ -226,7 +258,9 @@ class MudServer {
break; break;
default: default:
player.sendMessage(`Unknown command: ${command}. Type "help" for available commands.`); player.sendMessage(
`Unknown command: ${command}. Type "help" for available commands.`,
);
} }
player.sendPrompt(); player.sendPrompt();
@@ -257,7 +291,10 @@ class MudServer {
// Create HTTP server for serving the client // Create HTTP server for serving the client
const server = http.createServer((req, res) => { const server = http.createServer((req, res) => {
// let filePath = path.join(__dirname, "public", req.url === "/" ? "index.html" : req.url); // let filePath = path.join(__dirname, "public", req.url === "/" ? "index.html" : req.url);
let filePath = path.join("public", req.url === "/" ? "index.html" : req.url); let filePath = path.join(
"public",
req.url === "/" ? "index.html" : req.url,
);
const ext = path.extname(filePath); const ext = path.extname(filePath);
const contentTypes = { const contentTypes = {

View File

@@ -1,5 +1,5 @@
export function withSides(sides) { export function withSides(sides) {
const r = Math.random() const r = Math.random();
return Math.floor(r * sides) + 1; return Math.floor(r * sides) + 1;
} }
@@ -10,4 +10,3 @@ export function d6() {
export function d8() { export function d8() {
return withSides(8); return withSides(8);
} }

View File

@@ -2,7 +2,10 @@ export function cleanName(s) {
if (typeof s !== "string") { if (typeof s !== "string") {
throw new Error("String expected, but got a ", typeof s); throw new Error("String expected, but got a ", typeof s);
} }
return s.toLowerCase().replace(" ", "_").replace(/[^a-zA-Z0-9_]/, "_"); return s
.toLowerCase()
.replace(" ", "_")
.replace(/[^a-zA-Z0-9_]/, "_");
} }
/** /**

View File

@@ -65,28 +65,36 @@ export class ClientMessage {
return this._arr[0]; return this._arr[0];
} }
/** /**
* @param {string} msgData the raw text data in the websocket message. * @param {string} msgData the raw text data in the websocket message.
*/ */
constructor(msgData) { constructor(msgData) {
if (typeof msgData !== "string") { if (typeof msgData !== "string") {
throw new Error("Could not create client message. Attempting to parse json, but data was not even a string, it was a " + typeof msgData); throw new Error(
return "Could not create client message. Attempting to parse json, but data was not even a string, it was a " +
typeof msgData,
);
return;
} }
try { try {
this._arr = JSON.parse(msgData); this._arr = JSON.parse(msgData);
} catch (_) { } catch (_) {
throw new Error(`Could not create client message. Attempting to parse json, but data was invalid json: >>> ${msgData} <<<`); throw new Error(
`Could not create client message. Attempting to parse json, but data was invalid json: >>> ${msgData} <<<`,
);
} }
if (typeof this._arr !== "array") { if (typeof this._arr !== "array") {
throw new Error(`Could not create client message. Excpected an array, but got a ${typeof this._arr}`); throw new Error(
`Could not create client message. Excpected an array, but got a ${typeof this._arr}`,
);
} }
if (this._arr.length < 1) { if (this._arr.length < 1) {
throw new Error("Could not create client message. Excpected an array with at least 1 element, but got an empty one"); throw new Error(
"Could not create client message. Excpected an array with at least 1 element, but got an empty one",
);
} }
this._arr = arr; this._arr = arr;

View File

@@ -1,9 +1,9 @@
import { randomBytes, pbkdf2Sync, randomInt } from 'node:crypto'; import { randomBytes, pbkdf2Sync, randomInt } from "node:crypto";
// Settings (tune as needed) // Settings (tune as needed)
const ITERATIONS = 100_000; // Slow enough to deter brute force const ITERATIONS = 100_000; // Slow enough to deter brute force
const KEYLEN = 64; // 512-bit hash const KEYLEN = 64; // 512-bit hash
const DIGEST = 'sha512'; const DIGEST = "sha512";
/** /**
* Generate a hash from a plaintext password. * Generate a hash from a plaintext password.
@@ -11,8 +11,14 @@ const DIGEST = 'sha512';
* @returns String * @returns String
*/ */
export function hash(password) { export function hash(password) {
const salt = randomBytes(16).toString('hex'); // 128-bit salt const salt = randomBytes(16).toString("hex"); // 128-bit salt
const hash = pbkdf2Sync(password, salt, ITERATIONS, KEYLEN, DIGEST).toString('hex'); const hash = pbkdf2Sync(
password,
salt,
ITERATIONS,
KEYLEN,
DIGEST,
).toString("hex");
return `${ITERATIONS}:${salt}:${hash}`; return `${ITERATIONS}:${salt}:${hash}`;
} }
@@ -24,7 +30,13 @@ export function hash(password) {
* @returns Boolean * @returns Boolean
*/ */
export function verify(password, hashed_password) { export function verify(password, hashed_password) {
const [iterations, salt, hash] = hashed_password.split(':'); const [iterations, salt, hash] = hashed_password.split(":");
const derived = pbkdf2Sync(password, salt, Number(iterations), KEYLEN, DIGEST).toString('hex'); const derived = pbkdf2Sync(
password,
salt,
Number(iterations),
KEYLEN,
DIGEST,
).toString("hex");
return hash === derived; return hash === derived;
} }