Skip to content

Diagnose reachability for the request that called it - certificate, origin, secure context.

GET
/v1/admin/access-check
curl --request GET \
--url http://localhost:8080/v1/admin/access-check \
--header 'Authorization: Bearer <token>'

§S6b. Turns three invisible failures into three sentences, for THIS request: the Host and Origin seen, whether that host is covered by the served certificate’s SANs, the fingerprint and days-to-expiry, whether the origin would pass the /v1/signal allow-list, and whether that allow-list is configured at all.

TOPOLOGY DETECTION (§S6-0). When the request arrives with X-Forwarded-Proto or RFC 7239 Forwarded, a proxy is in front of this control plane, so its certificate is an internal detail the browser never validates: certificate.in_use is FALSE with a reason saying the setup is supported and complete, and the answer focuses entirely on origins. Telling an operator to fix something already correct is the failure mode this endpoint exists to prevent.

FORWARDED HEADERS SOFTEN ADVICE AND NOTHING ELSE. They are trivially spoofable by any client; the worst a hostile one achieves is making an admin-gated diagnostics page tell THEM that their own certificate is not in use. No access decision, on this route or any other, reads them.

Admin-gated by the existing RequireAuth -> RequireAdmin middleware. It reflects the Host and Origin headers, so both are LENGTH-CAPPED (256 chars) and are only ever rendered as JSON string values - a client must never put them through dangerouslySetInnerHTML. It discloses configuration an admin can already read.

OK.

Media typeapplication/json
object
request
required
object
host
required

The Host header seen, LENGTH-CAPPED at 256 characters (a truncated value is suffixed). Reflected input - render as text, never as HTML.

string
origin
required

The Origin header seen, or “” when the client sent none. Same cap and same rule as host.

string
scheme
required

What THIS control plane observed on the wire - https when the request arrived on the TLS listener. Observed, never taken from a header.

string
Allowed values: http https
forwarded_proto

The protocol an upstream proxy CLAIMS the browser used (X-Forwarded-Proto, or the proto parameter of RFC 7239 Forwarded). A CLAIM, not a fact - it may soften advice and authorises nothing. Absent when no proxy header was present.

string
tls_terminated_upstream
required

§S6-0 topology C: a proxy is in front of this control plane. When true the certificate section reports in_use=false and a client MUST NOT present the internal certificate as a problem.

boolean
secure_context
required

Whether the browser will treat the page as a secure context - the precondition for getUserMedia (the microphone, §S7) and Keyboard Lock. True for https to this listener or https to an upstream proxy. THE LOOPBACK EXCEPTION IS TAKEN FROM THE BROWSER’S ORIGIN, NEVER FROM THE HOST HEADER once a forwarded protocol shows a proxy is in play: a proxy passing to 127.0.0.1 rewrites Host to a loopback authority, which would otherwise report an insecure public page as secure. That false positive is expensive out of proportion - it tells the operator the microphone should work and sends them hunting a bug that is not there. This is the link between the certificate story and the microphone story, and why they belong on one panel.

boolean
certificate
required
object
in_use
required

False under topology C, and false when QUASAR_TLS=off. Both are supported, complete configurations.

boolean
not_in_use_reason

Operator-facing prose stating that the setup is supported and what to look at instead. Present only when in_use is false.

string
info

The PUBLIC metadata of a certificate. Every field here is already disclosed by any TLS handshake with this listener. THERE IS DELIBERATELY NO FIELD THAT COULD HOLD KEY MATERIAL, and adding one would be the bug this shape exists to prevent.

object
source
required

Self_signed = the batteries-included pair generated into QUASAR_TLS_DIR on first boot; provided = files mounted at QUASAR_TLS_CERT/QUASAR_TLS_KEY. (A third value, uploaded, is reserved for the §S6d upload route and is not emitted by any server that does not serve it.)

string
Allowed values: self_signed provided
fingerprint_sha256
required

SHA-256 over the DER, uppercase colon-separated hex. The value to compare OUT OF BAND against the control-plane startup log before trusting a self-signed certificate.

string
subject
string
issuer
string
not_before
required
string format: date-time
not_after
required
string format: date-time
days_until_expiry
required

Recomputed on every read, never served frozen at install time. This is the number that catches the 90-day Let’s Encrypt trap, and a stale one would be worse than none. Can be negative for an already-expired certificate loaded from disk (upload refuses one).

integer
dns_names
required
Array<string>
ip_addresses
required
Array<string>
chain_length
required

Leaf plus any intermediates supplied with it.

integer
self_signed
required

The leaf issued itself - true for the batteries-included pair, and the reason a browser warns.

boolean
host_covered

Whether the request’s Host is covered by the leaf’s SANs. Present only when in_use is true. A leaf with no SANs covers NOTHING - the legacy Common Name fallback is dead in every current browser, so reporting CN coverage would tell an operator their setup is fine while their browser refuses it.

boolean | null
advice

The remedy, when there is one, in operator language. ORDERED BY WHAT BLOCKS THE OPERATOR, VALIDITY FIRST: already-expired, then near-expiry, then SAN mismatch, then self-signed, with NOT-YET-VALID ahead of all of them - a post-dated or expired certificate cannot be rescued by trusting it or by correcting its names, and both are reachable because the on-disk compatibility path deliberately keeps serving such a pair rather than refusing to boot. Also SOURCE-SPECIFIC: the fix for a SAN mismatch depends entirely on where the certificate came from, so this branches on info.source. For self_signed it names the QUASAR_TLS_HOSTS value AND the fact that the certificate is NEVER regenerated when that variable changes (delete the pem files and recreate the container) - the single most expensive unstated fact in this area. For provided it says QUASAR_TLS_HOSTS does not apply and the mounted file must be re-issued. A single remedy would send operators of the other supported topologies down steps that cannot work.

string
origins
required
object
configured
required

Whether any allow-list entry is in force. FALSE IS A NORMAL, WORKING STATE - see same_origin_exemption.

boolean
source
required

Which source won. environment means QUASAR_ALLOWED_ORIGINS is SET and the admin-editable column is not consulted; a UI should grey out its editor and say so.

string
Allowed values: environment database
allowed
required

The RESOLVED list /v1/signal actually enforces.

Array<string>
request_origin_allowed
required

Whether THIS request’s Origin would pass. Null when the request carried no Origin header at all (a non-browser client, which /v1/signal admits).

boolean | null
same_origin_exemption
required

True when the origin passes ONLY because it matches Host, not because it is listed. This is the case that silently breaks behind a Host-rewriting reverse proxy, and it is why “always seed the IP as an allowed origin” would be a no-op that looks load-bearing - a same-origin request already passes with NO configuration.

boolean
advice
string
suggested_entry

The current origin, normalized and ready to add, when it would otherwise be refused - detection plus the fix, not detection alone. Empty when there is nothing to suggest, and deliberately empty when the list is pinned by the environment, because adding it in the UI would do nothing.

string
Example
{
"request": {
"scheme": "http"
},
"certificate": {
"info": {
"source": "self_signed"
}
},
"origins": {
"source": "environment"
}
}

Missing/invalid/expired/revoked token.

Media typeapplication/json
object
error
required
object
code
required

E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.

string format: uuid
derived_tiles

Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.

Array<object>
object
id
required
string format: uuid
name
required
string
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"live_sessions": 1,
"session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"derived_tiles": [
{
"id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"name": "example"
}
]
}
}

Authenticated but insufficient role / not the owner (precedes resource lookup).

Media typeapplication/json
object
error
required
object
code
required

E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.

string format: uuid
derived_tiles

Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.

Array<object>
object
id
required
string format: uuid
name
required
string
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"live_sessions": 1,
"session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"derived_tiles": [
{
"id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"name": "example"
}
]
}
}