diff --git a/static/client.html b/static/client.html
index d05581e..7e8f92c 100644
--- a/static/client.html
+++ b/static/client.html
@@ -255,6 +255,11 @@
var urlParams = new URLSearchParams(window.location.search);
var shareToken = urlParams.get('token');
var apiKey = sessionStorage.getItem('rustguac_api_key');
+ // Pre-minted single-use WebSocket ticket passed in the URL. Lets a
+ // headless API integration authenticate the owner connection without an
+ // OIDC session or a stored API key: the backend mints it via
+ // POST /api/ws-ticket and hands the browser /client/{id}?ticket=...
+ var urlTicket = urlParams.get('ticket');
var entryName = urlParams.get('name');
// Populated from /api/sessions/:id when the session is backed by a
// Connections entry. Used by the Reconnect button to launch a fresh
@@ -311,7 +316,13 @@
});
}
- if (shareToken) {
+ if (urlTicket) {
+ // Headless API integration: the pre-minted ticket authenticates the
+ // WebSocket directly. Skip the /api/sessions metadata fetch — it
+ // needs its own auth, and routing the one-shot ticket through it
+ // would consume it before the WebSocket. Connect straight away.
+ startGuacamole();
+ } else if (shareToken) {
fetch('/api/sessions/' + sessionId + '/banner?token=' + encodeURIComponent(shareToken))
.then(function(res) { return res.ok ? res.json() : null; })
.then(function(data) { return showBanner(data && data.banner); })
@@ -357,6 +368,13 @@
function startGuacamole() {
statusEl.textContent = 'Connecting to session ' + sessionId.substring(0, 8) + '...';
+ // Pre-minted WebSocket ticket from the URL (headless API integration)
+ // takes priority: use it directly as the owner credential.
+ if (urlTicket) {
+ connectWebSocket(urlTicket);
+ return;
+ }
+
// API key users: exchange key for a single-use ticket before connecting.
// This keeps the API key out of the WebSocket URL (visible in logs).
if (apiKey) {