Release Healer Man 0.1.5

This commit is contained in:
phenom
2026-08-18 16:22:19 -04:00
parent f4c9cf356c
commit 55cfd43d66
62 changed files with 3030 additions and 153 deletions
+98 -2
View File
@@ -3,6 +3,10 @@ import { mkdirSync } from "node:fs";
import path from "node:path";
import { promisify } from "node:util";
import { DatabaseSync } from "node:sqlite";
import {
cloneDefaultManastormAdminConfig,
normalizeManastormAdminConfig,
} from "./manastorm-config.mjs";
const scrypt = promisify(scryptCallback);
@@ -33,8 +37,19 @@ async function derivePassword(password, salt) {
return Buffer.from(await scrypt(password, salt, 64));
}
const GM_USERNAME_KEYS = new Set(["phenom"]);
function accountRoles(username) {
return GM_USERNAME_KEYS.has(String(username ?? "").trim().toLowerCase()) ? ["gm"] : [];
}
function publicAccount(row) {
return { id: row.id, username: row.username, createdAt: Number(row.created_at) };
return {
id: row.id,
username: row.username,
createdAt: Number(row.created_at ?? row.createdAt),
roles: accountRoles(row.username),
};
}
export function openGameDatabase(options = {}) {
@@ -62,7 +77,21 @@ export function openGameDatabase(options = {}) {
account_id TEXT PRIMARY KEY REFERENCES accounts(id) ON DELETE CASCADE,
revision INTEGER NOT NULL, data_json TEXT NOT NULL, updated_at INTEGER NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS manastorm_admin_config (
id INTEGER PRIMARY KEY CHECK (id = 1),
schema_version INTEGER NOT NULL,
revision INTEGER NOT NULL,
config_json TEXT NOT NULL,
updated_by_account_id TEXT REFERENCES accounts(id) ON DELETE SET NULL,
updated_by_username TEXT,
updated_at INTEGER
) STRICT;
`);
database.prepare(`
INSERT OR IGNORE INTO manastorm_admin_config (
id, schema_version, revision, config_json, updated_by_account_id, updated_by_username, updated_at
) VALUES (1, 1, 0, ?, NULL, NULL, NULL)
`).run(JSON.stringify(cloneDefaultManastormAdminConfig()));
const statements = {
accountByUsername: database.prepare("SELECT * FROM accounts WHERE username_key = ?"),
@@ -82,6 +111,18 @@ export function openGameDatabase(options = {}) {
data_json = excluded.data_json, updated_at = excluded.updated_at
RETURNING revision, updated_at
`),
manastormConfig: database.prepare("SELECT * FROM manastorm_admin_config WHERE id = 1"),
updateManastormConfig: database.prepare(`
UPDATE manastorm_admin_config
SET schema_version = 1,
revision = revision + 1,
config_json = ?,
updated_by_account_id = ?,
updated_by_username = ?,
updated_at = ?
WHERE id = 1 AND revision = ?
RETURNING revision, updated_at
`),
};
function issueSession(accountId) {
@@ -110,7 +151,7 @@ export function openGameDatabase(options = {}) {
}
throw error;
}
return { account, ...issueSession(account.id) };
return { account: publicAccount(account), ...issueSession(account.id) };
}
async function login(usernameInput, password) {
@@ -160,6 +201,58 @@ export function openGameDatabase(options = {}) {
return { revision: Number(row.revision), updatedAt: Number(row.updated_at), data };
}
function isGameMaster(account) {
// Recompute authority from the server allowlist for every privileged call.
return accountRoles(account?.username).includes("gm");
}
function getManastormConfig() {
const row = statements.manastormConfig.get();
try {
return {
revision: Number(row?.revision ?? 0),
updatedAt: row?.updated_at == null ? null : Number(row.updated_at),
updatedBy: row?.updated_by_username ?? null,
config: normalizeManastormAdminConfig(JSON.parse(row?.config_json ?? "null")),
};
} catch (error) {
console.error("Healer Man Manastorm configuration is corrupt; using safe defaults", error);
return { revision: 0, updatedAt: null, updatedBy: null, config: cloneDefaultManastormAdminConfig() };
}
}
function putManastormConfig(account, expectedRevision, config) {
if (!isGameMaster(account)) {
throw new GameDatabaseError("Game Master access is required.", 403, "gm_required");
}
if (!Number.isInteger(expectedRevision) || expectedRevision < 0) {
throw new GameDatabaseError("A valid expected revision is required.", 400, "invalid_revision");
}
let normalized;
try {
normalized = normalizeManastormAdminConfig(config);
} catch (error) {
throw new GameDatabaseError(error instanceof Error ? error.message : "Invalid Manastorm configuration.", 400, "invalid_manastorm_config");
}
const now = Date.now();
const row = statements.updateManastormConfig.get(
JSON.stringify(normalized),
account.id,
account.username,
now,
expectedRevision,
);
if (!row) {
throw new GameDatabaseError("The Manastorm configuration changed. Reload and try again.", 409, "revision_conflict");
}
return {
revision: Number(row.revision),
updatedAt: Number(row.updated_at),
updatedBy: account.username,
config: normalized,
};
}
return {
databasePath,
register,
@@ -168,6 +261,9 @@ export function openGameDatabase(options = {}) {
logout: (token) => { if (typeof token === "string" && token) statements.deleteSession.run(tokenHash(token)); },
getCloudSave,
putCloudSave,
isGameMaster,
getManastormConfig,
putManastormConfig,
pruneExpiredSessions: () => Number(statements.deleteExpiredSessions.run(Date.now()).changes),
close: () => database.close(),
};