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:
@@ -47,12 +47,15 @@ import {
|
||||
type DiscordNotificationService
|
||||
} from "../notifications/discord.js";
|
||||
import { safeErrorMessage } from "./logging.js";
|
||||
import type { ServerRuleRegistry } from "./rules.js";
|
||||
|
||||
export interface YonexusServerRuntimeOptions {
|
||||
config: YonexusServerConfig;
|
||||
store: YonexusServerStore;
|
||||
transport: ServerTransport;
|
||||
notificationService?: DiscordNotificationService;
|
||||
ruleRegistry?: ServerRuleRegistry;
|
||||
onClientAuthenticated?: (identifier: string) => void;
|
||||
now?: () => number;
|
||||
sweepIntervalMs?: number;
|
||||
}
|
||||
@@ -447,7 +450,7 @@ export class YonexusServerRuntime {
|
||||
);
|
||||
record.recentHandshakeAttempts.push(now);
|
||||
|
||||
if (record.recentHandshakeAttempts.length > AUTH_MAX_ATTEMPTS_PER_WINDOW) {
|
||||
if (record.recentHandshakeAttempts.length >= AUTH_MAX_ATTEMPTS_PER_WINDOW) {
|
||||
await this.triggerRePairRequired(connection, record, envelope.requestId, "rate_limited");
|
||||
return;
|
||||
}
|
||||
@@ -543,7 +546,10 @@ export class YonexusServerRuntime {
|
||||
session.lastActivityAt = now;
|
||||
session.publicKey = publicKey;
|
||||
}
|
||||
this.options.transport.promoteToAuthenticated(identifier, connection.ws);
|
||||
const promoted = this.options.transport.promoteToAuthenticated(identifier, connection.ws);
|
||||
if (promoted) {
|
||||
this.options.onClientAuthenticated?.(identifier);
|
||||
}
|
||||
this.options.transport.sendToConnection(
|
||||
{ ...connection, identifier },
|
||||
encodeBuiltin(
|
||||
@@ -613,6 +619,11 @@ export class YonexusServerRuntime {
|
||||
this.pairingService.markNotificationFailed(record);
|
||||
}
|
||||
|
||||
// Persist immediately so the pairing code is readable from disk (e.g. via CLI)
|
||||
if (!reusePending) {
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
this.options.transport.sendToConnection(
|
||||
connection,
|
||||
encodeBuiltin(
|
||||
@@ -620,7 +631,7 @@ export class YonexusServerRuntime {
|
||||
{
|
||||
identifier: record.identifier,
|
||||
expiresAt: request.expiresAt,
|
||||
ttlSeconds: this.pairingService.getRemainingTtl(record),
|
||||
ttlSeconds: request.ttlSeconds,
|
||||
adminNotification: notified ? "sent" : "failed",
|
||||
codeDelivery: "out_of_band"
|
||||
},
|
||||
@@ -628,22 +639,8 @@ export class YonexusServerRuntime {
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (!notified) {
|
||||
this.options.transport.sendToConnection(
|
||||
connection,
|
||||
encodeBuiltin(
|
||||
buildPairFailed(
|
||||
{
|
||||
identifier: record.identifier,
|
||||
reason: "admin_notification_failed"
|
||||
},
|
||||
{ requestId, timestamp: this.now() }
|
||||
)
|
||||
)
|
||||
);
|
||||
this.pairingService.clearPairingState(record);
|
||||
}
|
||||
// Pairing remains pending regardless of notification status.
|
||||
// The admin can retrieve the pairing code via the server CLI command.
|
||||
}
|
||||
|
||||
private async handleHeartbeat(
|
||||
@@ -932,16 +929,8 @@ export class YonexusServerRuntime {
|
||||
const parsed = parseRuleMessage(raw);
|
||||
const rewritten = `${parsed.ruleIdentifier}::${senderIdentifier}::${parsed.content}`;
|
||||
|
||||
// TODO: Dispatch to registered rules via rule registry
|
||||
// For now, just log the rewritten message
|
||||
// this.ruleRegistry.dispatch(rewritten);
|
||||
|
||||
// Update last activity
|
||||
session.lastActivityAt = this.now();
|
||||
|
||||
// Future: dispatch to rule registry
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
void rewritten;
|
||||
this.options.ruleRegistry?.dispatch(rewritten);
|
||||
} catch (error) {
|
||||
// Malformed rule message
|
||||
this.options.transport.sendToConnection(
|
||||
|
||||
Reference in New Issue
Block a user