fix(relay): proper gateway protocol handshake with role/scopes/ticks
- Add role: operator, scopes: [operator.read, operator.write] - Handle connect.challenge event from gateway - Add tick keepalive (gateway expects periodic ticks) - Fix duplicate message listener bug - Fix default URL fallback (remove non-existent ws. subdomain)
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
* (BetterAuth) (relay) (token auth)
|
* (BetterAuth) (relay) (token auth)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const GATEWAY_URL = process.env.GATEWAY_WS_URL || process.env.VITE_WS_URL || "wss://ws.hammer.donovankelly.xyz";
|
const GATEWAY_URL = process.env.GATEWAY_WS_URL || process.env.VITE_WS_URL || "wss://hammer.donovankelly.xyz";
|
||||||
const GATEWAY_TOKEN = process.env.GATEWAY_WS_TOKEN || process.env.VITE_WS_TOKEN || "";
|
const GATEWAY_TOKEN = process.env.GATEWAY_WS_TOKEN || process.env.VITE_WS_TOKEN || "";
|
||||||
|
|
||||||
type GatewayState = "disconnected" | "connecting" | "connected";
|
type GatewayState = "disconnected" | "connecting" | "connected";
|
||||||
@@ -28,6 +28,8 @@ class GatewayConnection {
|
|||||||
private eventListeners = new Set<MessageHandler>();
|
private eventListeners = new Set<MessageHandler>();
|
||||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
private shouldReconnect = true;
|
private shouldReconnect = true;
|
||||||
|
private connectSent = false;
|
||||||
|
private tickTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.connect();
|
this.connect();
|
||||||
@@ -36,6 +38,7 @@ class GatewayConnection {
|
|||||||
private connect() {
|
private connect() {
|
||||||
if (this.state === "connecting") return;
|
if (this.state === "connecting") return;
|
||||||
this.state = "connecting";
|
this.state = "connecting";
|
||||||
|
this.connectSent = false;
|
||||||
|
|
||||||
if (!GATEWAY_TOKEN) {
|
if (!GATEWAY_TOKEN) {
|
||||||
console.warn("[gateway-relay] No GATEWAY_WS_TOKEN set, chat relay disabled");
|
console.warn("[gateway-relay] No GATEWAY_WS_TOKEN set, chat relay disabled");
|
||||||
@@ -56,6 +59,58 @@ class GatewayConnection {
|
|||||||
|
|
||||||
this.ws.addEventListener("open", () => {
|
this.ws.addEventListener("open", () => {
|
||||||
console.log("[gateway-relay] WebSocket open, sending handshake...");
|
console.log("[gateway-relay] WebSocket open, sending handshake...");
|
||||||
|
this.sendConnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.addEventListener("message", (event) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(String(event.data));
|
||||||
|
|
||||||
|
// Handle connect.challenge — gateway may send this before we connect
|
||||||
|
if (msg.type === "event" && msg.event === "connect.challenge") {
|
||||||
|
console.log("[gateway-relay] Received connect challenge");
|
||||||
|
// Token auth doesn't need signing; send connect if not yet sent
|
||||||
|
if (!this.connectSent) {
|
||||||
|
this.sendConnect();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleMessage(msg);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[gateway-relay] Failed to parse message:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.addEventListener("close", () => {
|
||||||
|
console.log("[gateway-relay] Disconnected");
|
||||||
|
this.state = "disconnected";
|
||||||
|
this.ws = null;
|
||||||
|
this.connectSent = false;
|
||||||
|
if (this.tickTimer) {
|
||||||
|
clearInterval(this.tickTimer);
|
||||||
|
this.tickTimer = null;
|
||||||
|
}
|
||||||
|
// Reject all pending requests
|
||||||
|
for (const [id, pending] of this.pendingRequests) {
|
||||||
|
clearTimeout(pending.timer);
|
||||||
|
pending.reject(new Error("Connection closed"));
|
||||||
|
this.pendingRequests.delete(id);
|
||||||
|
}
|
||||||
|
if (this.shouldReconnect) {
|
||||||
|
this.scheduleReconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.addEventListener("error", () => {
|
||||||
|
console.error("[gateway-relay] WebSocket error");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendConnect() {
|
||||||
|
if (this.connectSent) return;
|
||||||
|
this.connectSent = true;
|
||||||
|
|
||||||
const connectId = nextReqId();
|
const connectId = nextReqId();
|
||||||
this.sendRaw({
|
this.sendRaw({
|
||||||
type: "req",
|
type: "req",
|
||||||
@@ -66,12 +121,17 @@ class GatewayConnection {
|
|||||||
maxProtocol: 3,
|
maxProtocol: 3,
|
||||||
client: {
|
client: {
|
||||||
id: "dashboard-relay",
|
id: "dashboard-relay",
|
||||||
displayName: "Hammer Dashboard Relay",
|
displayName: "Hammer Dashboard",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
platform: "server",
|
platform: "server",
|
||||||
mode: "webchat",
|
mode: "webchat",
|
||||||
instanceId: `relay-${process.pid}-${Date.now()}`,
|
instanceId: `relay-${process.pid}-${Date.now()}`,
|
||||||
},
|
},
|
||||||
|
role: "operator",
|
||||||
|
scopes: ["operator.read", "operator.write"],
|
||||||
|
caps: [],
|
||||||
|
commands: [],
|
||||||
|
permissions: {},
|
||||||
auth: {
|
auth: {
|
||||||
token: GATEWAY_TOKEN,
|
token: GATEWAY_TOKEN,
|
||||||
},
|
},
|
||||||
@@ -80,9 +140,15 @@ class GatewayConnection {
|
|||||||
|
|
||||||
// Wait for handshake response
|
// Wait for handshake response
|
||||||
this.pendingRequests.set(connectId, {
|
this.pendingRequests.set(connectId, {
|
||||||
resolve: () => {
|
resolve: (payload) => {
|
||||||
console.log("[gateway-relay] Connected to gateway");
|
console.log("[gateway-relay] Connected to gateway, protocol:", payload?.protocol);
|
||||||
this.state = "connected";
|
this.state = "connected";
|
||||||
|
|
||||||
|
// Start tick keepalive (gateway expects periodic ticks)
|
||||||
|
const tickInterval = payload?.policy?.tickIntervalMs || 15000;
|
||||||
|
this.tickTimer = setInterval(() => {
|
||||||
|
this.sendRaw({ type: "tick" });
|
||||||
|
}, tickInterval);
|
||||||
},
|
},
|
||||||
reject: (err) => {
|
reject: (err) => {
|
||||||
console.error("[gateway-relay] Handshake failed:", err);
|
console.error("[gateway-relay] Handshake failed:", err);
|
||||||
@@ -98,35 +164,6 @@ class GatewayConnection {
|
|||||||
}
|
}
|
||||||
}, 15000),
|
}, 15000),
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
this.ws.addEventListener("message", (event) => {
|
|
||||||
try {
|
|
||||||
const msg = JSON.parse(String(event.data));
|
|
||||||
this.handleMessage(msg);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[gateway-relay] Failed to parse message:", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ws.addEventListener("close", () => {
|
|
||||||
console.log("[gateway-relay] Disconnected");
|
|
||||||
this.state = "disconnected";
|
|
||||||
this.ws = null;
|
|
||||||
// Reject all pending requests
|
|
||||||
for (const [id, pending] of this.pendingRequests) {
|
|
||||||
clearTimeout(pending.timer);
|
|
||||||
pending.reject(new Error("Connection closed"));
|
|
||||||
this.pendingRequests.delete(id);
|
|
||||||
}
|
|
||||||
if (this.shouldReconnect) {
|
|
||||||
this.scheduleReconnect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ws.addEventListener("error", (e) => {
|
|
||||||
console.error("[gateway-relay] WebSocket error");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleReconnect() {
|
private scheduleReconnect() {
|
||||||
@@ -154,7 +191,7 @@ class GatewayConnection {
|
|||||||
if (msg.ok !== false) {
|
if (msg.ok !== false) {
|
||||||
pending.resolve(msg.payload ?? msg.result ?? {});
|
pending.resolve(msg.payload ?? msg.result ?? {});
|
||||||
} else {
|
} else {
|
||||||
pending.reject(new Error(msg.error?.message || "Request failed"));
|
pending.reject(new Error(msg.error?.message || msg.error || "Request failed"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (msg.type === "event") {
|
} else if (msg.type === "event") {
|
||||||
@@ -167,6 +204,7 @@ class GatewayConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Ignore tick responses and other frame types
|
||||||
}
|
}
|
||||||
|
|
||||||
isConnected(): boolean {
|
isConnected(): boolean {
|
||||||
@@ -200,6 +238,7 @@ class GatewayConnection {
|
|||||||
destroy() {
|
destroy() {
|
||||||
this.shouldReconnect = false;
|
this.shouldReconnect = false;
|
||||||
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
||||||
|
if (this.tickTimer) clearInterval(this.tickTimer);
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
try { this.ws.close(); } catch {}
|
try { this.ws.close(); } catch {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user