In STYGION Keystone
The interface — every route
All 44 paths of the local API: the scope each needs, what it takes, what it answers — and how to wire your own tooling to it, from this machine and from outside.
Updated
The interface — every route
This list is not written from memory. The mod generates a document about itself out of its own routes (GET /v1/openapi.json), so it cannot be wrong about what exists — and this page was pulled out of that with scripts/reference.sh in the mod's repository. When you are unsure, ask your server rather than this page.
Current for 0.1.1. There are 44 paths.
Before you call anything
1. Make a key
/keystone key new my-tool status.read players.read
A key is printed once and never again. Give it the narrowest set of scopes that does the job: a script watching whether the server is up wants status.read and nothing else.
2. Call it
curl -sS -H "Authorization: Bearer ks_…" \
http://127.0.0.1:25586/v1/status
GET takes its parameters in the address; POST sends them as a form:
curl -sS -X POST -H "Authorization: Bearer ks_…" \
-d "player=Steve" -d "group=vip" -d "for=30d" \
-d "note=bought on the site" \
http://127.0.0.1:25586/v1/players/group
3. What the answers mean
| Code | What it is |
|---|---|
200 |
It worked. |
400 |
Something in the request was wrong, and the answer says what. |
403 |
The key is real but does not hold the scope this route needs. |
404 |
No such route — or no key at all. Those answer identically on purpose, so an unauthenticated caller learns nothing about this server. |
429 |
Past what this key is allowed per minute. |
How to reach it
From this machine only (the default, and the safe one)
[api]
enabled = true
address = "127.0.0.1"
port = 25586
Nothing is visible from outside. The panel and your scripts run on the same machine.
From your own computer, over a tunnel (recommended on a hosted server)
Leave the mod on loopback and bring the port to you:
ssh -L 25586:127.0.0.1:25586 you@your-server
curl -sS -H "Authorization: Bearer ks_…" http://127.0.0.1:25586/v1/status
Nothing is exposed, and /keystone panel opens in your own browser.
From your home network
/keystone set keystone.api.address 0.0.0.0
/keystone set keystone.api.reachable-at 192.168.1.100
The first says where it listens; the second says which address the mod writes into the editor link and into openapi.json — 0.0.0.0 is a good answer to the first and a useless one to the second. Then open the port to the LAN only, not the world:
sudo ufw allow from 192.168.1.0/24 to any port 25586 proto tcp
From outside, behind a reverse proxy or a tunnel
Leave the mod on loopback and put something that speaks TLS in front of it:
location /keystone/ {
proxy_pass http://127.0.0.1:25586/;
proxy_set_header Host $host;
}
/keystone set keystone.api.reachable-at keystone.yourserver.eu
Never put api.address on a public address without TLS. The key travels in a header, and over plain http:// across the internet anybody on the path reads it. And the API can do everything the mod can do.
The live stream
curl -N -H "Authorization: Bearer ks_…" \
http://127.0.0.1:25586/v1/events
It holds the connection open and writes events down it as they happen (text/event-stream). Three lines in a browser:
const events = new EventSource('/v1/events');
events.onmessage = (e) => console.log(JSON.parse(e.data));
It is authenticated like everything else and answers 404 without a key.
Status and measurement
| Method | Path | Scope | What it is for |
|---|---|---|---|
GET |
/v1/status |
status.read |
What this server is and how it is doing |
GET |
/v1/metrics |
status.read |
What it is doing right now, and the last quarter of an hour |
GET |
/v1/metrics/history |
status.read |
A minute at a time, as far back as the settings keep it |
GET |
/v1/entities |
status.read |
What is loaded in the world, and what a sweep would remove right now |
GET |
/v1/gate |
status.read |
Whether the server is open, and how many places are held back |
GET |
/v1/catalog |
status.read |
Everything this pack has loaded: the registries, what is in one (?of=minecraft:item, with from/limit/like), or its tags (&tags=true) |
GET |
/v1/bench |
server.read |
Load test runs this server has made (?id= for one in full) |
GET |
/v1/openapi.json |
status.read |
This document |
curl -sS -H "Authorization: Bearer ks_…" \
"http://127.0.0.1:25586/v1/catalog?of=minecraft:item&like=diamond&limit=20"
People and permissions
| Method | Path | Scope | What it is for |
|---|---|---|---|
GET |
/v1/players |
players.read |
Who is online right now |
GET |
/v1/progress |
players.read |
What one player has done, or the board for one counter |
GET |
/v1/skins |
players.read |
What each player is wearing and where it came from |
GET |
/v1/accounts |
players.read |
How many accounts have a password, and who is waiting to prove themselves |
GET |
/v1/bans |
players.read |
Every ban that still stands |
GET |
/v1/tickets |
players.read |
The tickets on this server (?id= for one with everything said in it, ?player=) |
GET |
/v1/votes |
players.read |
Votes this server has been sent, and how the party is doing (?player=, ?limit=) |
GET |
/v1/discord/links |
players.read |
Which Minecraft accounts one Discord user has linked here (?discord=<id>) |
GET |
/v1/groups |
permissions.read |
Every permission group, heaviest first |
POST |
/v1/players/check |
permissions.read |
Whether somebody may do a thing, and which rule decided |
POST |
/v1/players/group |
permissions.write |
Put somebody in a group, or extend how long they are in it |
POST |
/v1/players/perk |
perks.write |
Give somebody a named benefit, optionally until a date |
POST |
/v1/players/grants |
perks.read |
What one player holds, where each came from and when it ends |
What they take:
| Path | Takes |
|---|---|
/v1/players/check |
player — the name of somebody online · permission — the permission to ask about |
/v1/players/grants |
player — the name of somebody online |
/v1/players/group |
player · group — the group id · for — 30d, 12h, 90m, or leave it out for forever · note — why, for whoever reads this later |
/v1/players/perk |
player · perk — what they get · for · note |
How to build a shop on it
On a server that is not in online mode, a name typed into a form proves nothing. The order is this:
# 1. Who that person is in game — by the Discord they signed in with
curl -sS -H "Authorization: Bearer ks_…" \
"http://127.0.0.1:25586/v1/discord/links?discord=123456789012345678"
# 2. Deliver what they bought, with an end date
curl -sS -X POST -H "Authorization: Bearer ks_…" \
-d "player=Steve" -d "group=vip" -d "for=30d" -d "note=order 4821" \
http://127.0.0.1:25586/v1/players/group
The rank ends on the day by itself. Nobody has to remember to take it back.
Operations
| Method | Path | Scope | What it is for |
|---|---|---|---|
GET |
/v1/operations |
operations.read |
What is scheduled and how long is left |
GET |
/v1/operations/history |
operations.read |
What has run lately, most recent first |
POST |
/v1/operations/run |
operations.run |
Run one now, or after a delay, with its usual warnings |
POST |
/v1/operations/cancel |
operations.run |
Call off what is pending and go back to the schedule |
POST |
/v1/gate/maintenance |
operations.run |
Close the server for maintenance, or open it again |
GET |
/v1/backups |
backup.read |
Every backup there is, newest first, and what they cost on disk |
POST |
/v1/backups/restore |
backup.restore |
Put a backup back — staged, and done at the start of the next boot |
What they take:
| Path | Takes |
|---|---|
/v1/operations/run |
operation — which one · in — how many seconds from now, 0 for immediately |
/v1/operations/cancel |
operation — which one |
/v1/gate/maintenance |
closed — true to close, false to open · reason — what is happening, shown to anybody who tries to join · until — when it is expected to end, in your own words |
/v1/backups/restore |
backup — the backup's name, or cancel to call one off |
Settings and the record
| Method | Path | Scope | What it is for |
|---|---|---|---|
GET |
/v1/settings |
settings.read |
Every setting every module declared, with what each one is and what it holds |
POST |
/v1/settings |
settings.write |
Change one. Refused with the reason if the value does not fit what it is |
GET |
/v1/settings/history |
settings.read |
Every settings change, newest first, with what each value was before it |
POST |
/v1/settings/rollback |
settings.write |
Undo what one version changed. Undoing is itself a version, so it can be undone |
POST |
/v1/reload |
settings.write |
Re-read every settings file. Nothing restarts and nobody is disconnected |
GET |
/v1/audit |
audit.read |
Who changed what over this interface, newest first. Reading is not recorded; changes and refusals are |
GET |
/v1/keys |
keys.read |
The keys this server has issued, without their secrets |
What they take: /v1/settings takes setting (its full name, such as operations.restart.when) and value; /v1/settings/rollback takes version.
curl -sS -X POST -H "Authorization: Bearer ks_…" \
-d "setting=operations.restart.when" -d "value=every 6h" \
http://127.0.0.1:25586/v1/settings
Everything there is to set is on every setting.
Console, chat and Discord
| Method | Path | Scope | What it is for |
|---|---|---|---|
GET |
/v1/console |
console.read |
The last lines this server printed, oldest first. Held in memory, not in the database |
GET |
/v1/chat |
chat.read |
The last things said in chat, oldest first |
GET |
/v1/events |
events.read |
The live stream of events |
GET |
/v1/discord |
server.read |
Whether the Discord bridge is connected, and how far behind it is |
The assistant
| Method | Path | Scope | What it is for |
|---|---|---|---|
POST |
/v1/assistant/ask |
assistant.write |
Ask it something. The answer comes back with this call |
POST |
/v1/assistant/incident |
assistant.write |
Hand it the last stretch of console and chat and ask what looks wrong |
POST |
/v1/assistant/decide |
assistant.write |
Agree to a proposed change, or leave it alone |
GET |
/v1/assistant/proposals |
assistant.read |
Changes it wants made and nobody has decided on yet |
GET |
/v1/assistant/threads |
assistant.read |
The conversations there have been, newest first |
GET |
/v1/assistant/thread |
assistant.read |
One conversation, oldest message first |
What they take: /v1/assistant/ask takes question; /v1/assistant/decide takes proposal (which one) and answer (yes or no).
Addons on the JVM
If you are building an addon in Java you do not talk over HTTP. You compile against keystone-api, which travels inside that same jar (META-INF/jarjar/eu.stygion.keystone.api-<version>.jar) and has no dependencies of its own, not even a logging framework. It is how you register your own module, command, permission, source of ranks — and your own routes, which then appear in this list and in openapi.json exactly like ours.
What is written down
Every change through the interface is an audit line: which key, what, when — and every refusal too. A key repeatedly trying what it may not do is the only sign you will get that an integration has been taken over. Sensitive commands are masked out of the audit, the console and the log.
Did this page help?
Opens the feedback panel with this page attached, and lands in the same queue as everything else.