feat: wire rule registry and client-authenticated callback into server runtime

- Add ruleRegistry and onClientAuthenticated options to YonexusServerRuntime
- Dispatch rewritten rule messages (rule::sender::content) to rule registry
- Guard onClientAuthenticated behind promoteToAuthenticated return value
- Fix transport message handler: use tempConn directly when ws is in temp state,
  preventing stale _connections entry from causing promoteToAuthenticated to fail
- Close competing temp connections with same identifier on promotion
- Expose __yonexusServer on globalThis for cross-plugin communication
- Remove auto-failure on admin notification miss; pairing stays pending

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
h z
2026-04-10 20:15:03 +01:00
parent 31f41cb49b
commit 59d5b26aff
6 changed files with 218 additions and 61 deletions

View File

@@ -186,6 +186,16 @@ export class YonexusServerTransport implements ServerTransport {
this._connections.delete(identifier);
}
// Also close any OTHER temp connections that claimed the same identifier.
// This handles the case where a second hello came in with the same identifier
// while the first was still in the temp/pairing phase.
for (const [otherWs, otherTemp] of this.tempConnections.entries()) {
if (otherWs !== ws && otherTemp.assignedIdentifier === identifier) {
otherWs.close(1008, "Connection replaced by new authenticated session");
this.tempConnections.delete(otherWs);
}
}
// Remove from temp connections
this.tempConnections.delete(ws);
@@ -229,22 +239,24 @@ export class YonexusServerTransport implements ServerTransport {
ws.on("message", (data: RawData) => {
const message = data.toString("utf8");
// Try to get identifier from temp connections first, then authenticated connections
let identifier: string | null = null;
const tempData = this.tempConnections.get(ws);
if (tempData) {
identifier = tempData.assignedIdentifier;
}
if (!identifier) {
for (const [id, conn] of this._connections) {
if (conn.ws === ws) {
identifier = id;
break;
}
}
// If this ws is still in temp state, use tempConn directly.
// Never fall through to _connections — it may hold a stale entry for the
// same identifier from a previously-authenticated session that hasn't
// finished closing yet, which would cause promoteToAuthenticated to receive
// the wrong WebSocket and silently fail.
if (this.tempConnections.has(ws)) {
this.options.onMessage(tempConn, message);
return;
}
const connection = identifier ? this._connections.get(identifier) ?? tempConn : tempConn;
// ws has been promoted — find it in authenticated connections
let connection: ClientConnection = tempConn;
for (const [, conn] of this._connections) {
if (conn.ws === ws) {
connection = conn;
break;
}
}
this.options.onMessage(connection, message);
});