EntwicklerTypeScript-SDK

Hostwolf-SDK für JavaScript & TypeScript

@hostwolf/sdk ist der offizielle typisierte Client für die Hostwolf-API. Damit steuerst du deine Gameserver per Code: Power, Konsole, Dateien, Backups, Mods, Spieler, Zeitpläne und mehr.

npm install https://hostwolf.net/dl/hostwolf-sdk-0.1.8.tgz

Das SDK wird von hostwolf.net statt aus der npm-Registry ausgeliefert. Der Befehl oben trägt es in deine package.json als "@hostwolf/sdk": "https://hostwolf.net/dl/hostwolf-sdk-0.1.8.tgz" ein, und du importierst es als @hostwolf/sdk. pnpm, yarn und bun akzeptieren dieselbe URL. Jede Version hat ihre eigene URL; siehe Versionen.

Es läuft auf Node 18+, Bun und Deno und hat keine Laufzeitabhängigkeiten. Für die Live-Konsole auf Node-Versionen vor 22 zusätzlich npm install ws ausführen.

Schnellstart#

Erstelle einen API-Schlüssel unter panel.hostwolf.net/account/api, dann:

import { Hostwolf } from '@hostwolf/sdk';

const hw = new Hostwolf({ apiKey: process.env.HOSTWOLF_API_KEY! });

const { data: servers } = await hw.servers.list();
for (const s of servers) {
    const res = await hw.servers.resources(s.identifier);
    console.log(s.name, s.egg_name, res.current_state);
}

const id = servers[0].identifier;
await hw.servers.command(id, 'say Backup in 1 minute');
await hw.backups.create(id, { name: 'before-update' });
console.log(await hw.tail(id, { lines: 20 }));

Jede Server-Methode nimmt als erstes Argument den kurzen identifier des Servers (die 8 Zeichen in seiner Panel-URL) oder seine UUID. Antwortobjekte verwenden die Feldnamen der API (snake_case). Ihre TypeScript-Typen werden exportiert, z. B. Server, Backup und FileObject.

Optionen#

new Hostwolf({
    apiKey: 'ptlc_...',                     // required
    baseUrl: 'https://panel.hostwolf.net',  // default
    timeoutMs: 30_000,                      // per request
    maxRetries: 2,                          // 429s, and 502/503/504 on GET
    maxRetryWaitMs: 10_000,                 // longer Retry-After waits fail fast instead
    fetch: customFetch,                     // optional
    WebSocket: customWebSocket,             // optional, for the console
});

Server#

MethodeWas sie tut
servers.list({ page, per_page })Eine Seite deiner Server: { data, pagination }
servers.listAll()Async-Iterator über alle Server
servers.get(id, ['egg', 'subusers'])Serverdetails mit Zuweisungen und Startvariablen
servers.resources(id)Live-Status, CPU, Arbeitsspeicher, Speicherplatz, Netzwerk, Laufzeit
servers.power(id, signal) / start / stop / restart / killPower-Aktionen
servers.command(id, command)Konsolenbefehl ausführen
servers.activity(id)Aktivitätsprotokoll (paginiert)
servers.rename(id, name, description?)Umbenennen
servers.reinstall(id)Installationsskript erneut ausführen
servers.setDockerImage(id, image)Laufzeit-Image wechseln (z. B. Java-Version)
servers.websocket(id)Konsolen-Token und Socket-URL (die Konsole unten nutzt sie)
servers.waitForState(id, 'running', { timeoutMs })Wartet, bis der Server einen Zustand erreicht, z. B. nach start(). Liefert seine Ressourcen
await hw.servers.start(id);
await hw.servers.waitForState(id, 'running');               // default timeout 5 minutes
const line = await hw.waitForConsole(id, /There are \d+ of a max/, { send: 'list' });

Konsole#

const c = await hw.console(id).connect();
c.on('output', (line) => console.log(line));
c.on('stats', (s) => console.log(s.state, s.memory_bytes));
c.requestLogs();          // replay recent output
c.sendCommand('list');
// later
c.close();

// Or just fetch the recent lines (ANSI colours stripped):
const lines = await hw.tail(id, { lines: 50 });

// Or wait for one line: send a command and get its reply.
const reply = await hw.waitForConsole(id, /There are \d+ of a max/, { send: 'list', timeoutMs: 20_000 });

waitForConsole(id, pattern, { send, includeRecent, timeoutMs }) liefert die erste Zeile, die zu einem String oder RegExp passt. includeRecent durchsucht auch das jüngste Log, und nach timeoutMs (standardmäßig 30 Sekunden) schlägt es mit dem Code Timeout fehl.

Die Konsole verbindet sich direkt mit dem Node deines Servers und erneuert ihr Token automatisch.

Events:

  • ready und close
  • output
  • status
  • stats
  • install output
  • daemon message und daemon error
  • error

Dateien#

await hw.files.list(id, '/plugins');
const props = await hw.files.read(id, 'server.properties');
await hw.files.write(id, 'server.properties', props.replace('pvp=true', 'pvp=false'));
await hw.files.rename(id, '/', [{ from: 'world', to: 'world-old' }]);
await hw.files.delete(id, '/logs', ['old.log']);
await hw.files.createFolder(id, '/', 'maps');
await hw.files.compress(id, '/', ['world']);       // returns the archive
await hw.files.decompress(id, '/', 'world.tar.gz');
await hw.files.pull(id, 'https://example.com/map.zip', { directory: '/maps' });
await hw.files.exists(id, 'plugins/LuckPerms.jar'); // true or false

// Binary files: upload and download go straight to the server's node.
await hw.files.upload(id, '/plugins', jarBytes, 'MyPlugin.jar');   // Blob, ArrayBuffer, Uint8Array or string
const buf = await hw.files.download(id, 'world.tar.gz');           // ArrayBuffer, in memory

// Or work with the signed URLs yourself, e.g. to stream a big file:
const url = await hw.files.downloadUrl(id, 'world.tar.gz');
const uploadUrl = await hw.files.uploadUrl(id);    // POST multipart "files" to `${uploadUrl}&directory=/maps`

Das Lesen einer nicht vorhandenen Datei wirft einen HostwolfError mit Status 404 und Code NotFound. Außerdem gibt es files.copy(id, path) und files.chmod(id, root, [{ file, mode }]).

Backups#

const { data } = await hw.backups.list(id);
const b = await hw.backups.create(id, { name: 'nightly', is_locked: false });
await hw.backups.waitUntilDone(id, b.uuid);   // polls until finished; rejects with code BackupFailed if it failed
await hw.backups.get(id, b.uuid);
await hw.backups.restore(id, b.uuid, { truncate: false });
await hw.backups.toggleLock(id, b.uuid);
await hw.backups.downloadUrl(id, b.uuid);
await hw.backups.delete(id, b.uuid);
await hw.backups.smartState(id);
await hw.backups.setSmart(id, true);   // automatic backups before risky changes

Du kannst 2 Backups pro 10 Minuten erstellen. Darüber hinaus schlägt create() sofort fehl, mit gesetztem error.retryAfter.

Datenbanken#

Jeder Server hat bis zu 5 MySQL-kompatible Datenbanken auf seinem eigenen Rechner. Die erste startet außerdem den Datenbankdienst, was etwa 40 Sekunden dauert: Die API antwortet mit 202 und create() gibt ein ausstehendes Ergebnis zurück, außer du übergibst wait.

import { isDatabasePending } from '@hostwolf/sdk';

// Simplest: wait until the database exists (up to 2 minutes), then use it
const db = await hw.databases.create(id, 'luckperms', { wait: true });
console.log(db.host.address, db.name, db.username, db.relationships?.password?.password);

// Or handle the pending answer yourself
const r = await hw.databases.create(id, 'coreprotect');
if (isDatabasePending(r)) {
    const ready = await hw.databases.waitUntilReady(id, 'coreprotect', { timeoutMs: 120_000 });
}

await hw.databases.service(id);   // { status, local_address, public_address, other_servers }
await hw.databases.setRemoteAccess(id, db.id, { remoteIps: ['203.0.113.7'], anywhere: false });
await hw.databases.rotatePassword(id, db.id);
const dump = await hw.databases.export(id, db.id);   // .sql.gz bytes
await hw.databases.import(id, db.id, dump, { filename: 'luckperms.sql.gz' });
await hw.databases.delete(id, db.id);

Gameserver verbinden sich mit db.host (172.18.0.1:3306). db.name trägt ein Präfix, zum Beispiel s12_luckperms; waitUntilReady() nimmt den Namen, den du angefragt hast. Die anderen Methoden nehmen db.id. waitUntilReady() schlägt mit dem Code DatabaseSetupFailed fehl, wenn der Dienst nicht starten konnte, oder mit Timeout. Du kannst 2 Datenbanken pro Minute anlegen und 6 pro Minute exportieren oder importieren.

Spiel, Spieler und Einstellungen#

await hw.game.versions(id);                          // current + available versions
await hw.game.setVersion(id, '1.21.4');              // switches and reinstalls; { resetWorld, force } for downgrades
await hw.game.settings(id);                          // friendly settings, grouped by file ({ supported: false } on Java Minecraft)
await hw.game.updateSettings(id, { difficulty: 'hard' }, true);
await hw.game.diagnostics(id);                       // "Fix my server" checks

await hw.players.info(id);                           // online count (+ names on Minecraft)
await hw.players.list(id);                           // live list via RCON/query
await hw.players.kick(id, 'Steve');
await hw.players.ban(id, { steamid: '7656...' });
await hw.players.unban(id, 'Steve');
await hw.players.broadcast(id, 'Restarting in 5 minutes');

Mods, Plugins und Modpacks#

const profile = await hw.addons.profile(id);         // sources, install dir, frameworks
const { items } = await hw.addons.search(id, 'modrinth', 'worldedit');
await hw.addons.install(id, 'modrinth', items[0].id); // newest compatible version
const installed = await hw.addons.installed(id, { checkUpdates: true });
await hw.addons.uninstall(id, installed.addons[0].id);

const packs = await hw.modpacks.search(id, 'cobblemon');
await hw.modpacks.install(id, packs.items[0].id, { freshWorld: false });

Weitere Addon-Aufrufe:

  • addons.versions(id, source, project): Versionen, als kompatibel markiert oder nicht.
  • addons.installFeatured(id, project): empfohlene Addons mit einem Klick, aus profile.featured.
  • addons.upload(id, blob, name): wenn profile.upload gesetzt ist. Sonst legst du Dateien mit files.upload() in profile.directory ab.
  • addons.removeManual(id, file): entfernt eine Datei, die du selbst hinzugefügt hast.
  • addons.setFramework(id, framework): z. B. Oxide oder Carbon auf Rust.
  • addons.workshop / setWorkshop / workshopPreview (Spiele mit Steam Workshop).
  • addons.cs2Framework(id, { repair }): Metamod + CounterStrikeSharp auf CS2.
  • modpacks.current / versions / remove.

Mehr#

NamespaceMethoden
startupget(id), setVariable(id, key, value)
scheduleslist, get, create, update, execute, delete, createTask, updateTask, deleteTask
databaseslist(id, { includePassword }), create, waitUntilReady, service, setRemoteAccess, rotatePassword, export, import, delete. Siehe Datenbanken
networkallocations, createAllocation, setNotes, setPrimary, deleteAllocation
userslist, get, create(id, publicId, permissions), update, delete, permissions(). Konten fügst du über ihre öffentliche ID (HW-XXXXXX) hinzu, nicht per E-Mail
notificationsDiscord-Benachrichtigungen pro Server: get, update, test
domainEigene Domain: get, check, set, remove
mcNetworkVelocity/BungeeCord-Netzwerke: get, link, unlink, installFabricProxy
accountget, updateEmail, updatePassword, updateProfile, updateLanguage(locale, currency?), uploadAvatar, deleteAvatar, activity, getNotifications, updateNotifications, testNotification, getCurseforgeKey, setCurseforgeKey, getFactorioCredentials, setFactorioCredentials
account.apiKeyslist, create(description, allowedIps?) (liefert den vollständigen Schlüssel einmalig als token), delete(identifier)
account.sshKeyslist, create(name, publicKey), delete(fingerprint)
storeNur lesend: catalog(), orders(), billing()

Server bestellen, kündigen und bezahlen geht nur im Panel, ein API-Schlüssel kann also nie Geld ausgeben.

Paginierung#

servers.list, backups.list und die activity-Methoden liefern { data, pagination }. So gehst du alle Server durch:

import { collect } from '@hostwolf/sdk';
const all = await collect(hw.servers.listAll());

Fehler#

Fehlgeschlagene Aufrufe werfen einen HostwolfError.

import { HostwolfError } from '@hostwolf/sdk';
try {
    await hw.game.setVersion(id, '1.16.5');
} catch (e) {
    if (e instanceof HostwolfError && e.code === 'downgrade_requires_reset') {
        // older versions can't load a newer world: retry with { resetWorld: true } or { force: true }
    }
}

Er hat diese Eigenschaften:

  • status: der HTTP-Status, oder 0 bei Netzwerkfehlern und Zeitüberschreitungen.
  • code: der Fehlercode. Neben den Codes der API verwendet das SDK NetworkError (keine Antwort oder Zeitüberschreitung), Timeout (ein waitFor…- oder waitUntil…-Helfer hat aufgegeben), BackupFailed, DatabaseSetupFailed, DatabaseNotCreated und NotFound (eine fehlende Datei).
  • retryAfter: bei einem 429 die Sekunden, bis die Aktion wieder erlaubt ist. Kurze Wartezeiten werden automatisch wiederholt; längere (wie das Limit von 2 Backups pro 10 Minuten) schlagen sofort fehl und setzen diesen Wert.
  • message: eine lesbare Beschreibung.
  • errors: die rohe Fehlerliste.
  • body: der Inhalt der Antwort.
  • Kurzabfragen: isNotFound, isRateLimited und isValidation.

Häufige Fälle:

  • 401: der Schlüssel ist falsch, unvollständig oder widerrufen.
  • 403 mit This IP address (…) does not have permission: der Schlüssel hat eine IP-Freigabeliste ohne deine Adresse.
  • 409 downgrade_requires_reset und 422 unknown_version: von game.setVersion().
  • 429: ein Ratenlimit; siehe retryAfter.
  • Ein Schreibaufruf mit Zeitüberschreitung (Status 0) kann auf dem Panel trotzdem fertig werden. Prüfe das, bevor du ihn wiederholst.

Direkte Anfragen#

Hat ein Endpunkt noch keine Methode, ruf ihn direkt auf:

await hw.http.get(`/servers/${id}/players`);
await hw.http.post(`/servers/${id}/command`, { command: 'list' });

Pfade sind relativ zu /api/client, und Antworten werden genauso entpackt wie bei den fertigen Methoden. Alle Endpunkte findest du in der API-Referenz.

Versionen#

DateiVerwendung
https://hostwolf.net/dl/hostwolf-sdk-.tgzEine feste Version. Verwende diese in package.json.
https://hostwolf.net/dl/hostwolf-sdk.tgzImmer die neueste Version. Paketmanager cachen nach URL, daher sind versionierte URLs besser.
https://hostwolf.net/dl/index.jsonAktuelle Versionen mit SHA-256- und npm-Integritäts-Hashes.

Zum Aktualisieren installierst du die neuere versionierte URL.

Lizenz#

MIT

Hängst du fest oder fehlt ein Endpunkt? Schreib uns unter game.hostwolf.net/contact oder support@hostwolf.net.