feat: wire rule registry and authenticated callback into client runtime
- Add ruleRegistry and onAuthenticated options to YonexusClientRuntime
- Dispatch non-builtin messages to rule registry
- Fire onAuthenticated callback on auth_success
- Reload persisted state on reconnect so externally-written secrets are picked up
- Re-send hello on auth_failed("not_paired") when client has a valid secret
- Always enter waiting_pair_confirm after pair_request regardless of notification status
- Expose __yonexusClient on globalThis for cross-plugin communication
- Wire onStateChange in transport creation (previously missing, prevented connection)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
} from "./state.js";
|
||||
import { generateNonce, signMessage } from "../crypto/keypair.js";
|
||||
import type { ClientConnectionState, ClientTransport } from "./transport.js";
|
||||
import type { ClientRuleRegistry } from "./rules.js";
|
||||
|
||||
export type YonexusClientPhase =
|
||||
| "idle"
|
||||
@@ -44,6 +45,8 @@ export interface YonexusClientRuntimeOptions {
|
||||
config: YonexusClientConfig;
|
||||
transport: ClientTransport;
|
||||
stateStore: YonexusClientStateStore;
|
||||
ruleRegistry?: ClientRuleRegistry;
|
||||
onAuthenticated?: () => void;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
@@ -118,6 +121,7 @@ export class YonexusClientRuntime {
|
||||
}
|
||||
|
||||
if (!isBuiltinMessage(raw)) {
|
||||
this.options.ruleRegistry?.dispatch(raw);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,6 +163,7 @@ export class YonexusClientRuntime {
|
||||
};
|
||||
await this.options.stateStore.save(this.clientState);
|
||||
this.phase = "authenticated";
|
||||
this.options.onAuthenticated?.();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -177,7 +182,17 @@ export class YonexusClientRuntime {
|
||||
|
||||
handleTransportStateChange(state: ClientConnectionState): void {
|
||||
if (state === "connected") {
|
||||
this.sendHello();
|
||||
// Reload state from disk before hello so that any secret written by an
|
||||
// external process (e.g. a pairing script) is picked up on reconnect.
|
||||
this.options.stateStore.load(this.options.config.identifier).then((fresh) => {
|
||||
if (fresh) {
|
||||
this.clientState = { ...this.clientState, ...fresh };
|
||||
}
|
||||
this.sendHello();
|
||||
}).catch(() => {
|
||||
// If reload fails, proceed with in-memory state
|
||||
this.sendHello();
|
||||
});
|
||||
}
|
||||
|
||||
if (state === "disconnected") {
|
||||
@@ -259,7 +274,10 @@ export class YonexusClientRuntime {
|
||||
adminNotification: payload.adminNotification
|
||||
};
|
||||
this.lastPairingFailure = undefined;
|
||||
this.phase = payload.adminNotification === "sent" ? "waiting_pair_confirm" : "pair_required";
|
||||
// Always wait for the pairing code regardless of notification status.
|
||||
// When adminNotification is "failed", the admin can retrieve the code
|
||||
// via the server CLI command and deliver it through an alternate channel.
|
||||
this.phase = "waiting_pair_confirm";
|
||||
}
|
||||
|
||||
private async handlePairSuccess(envelope: TypedBuiltinEnvelope<"pair_success">): Promise<void> {
|
||||
@@ -316,6 +334,12 @@ export class YonexusClientRuntime {
|
||||
}
|
||||
|
||||
this.lastPairingFailure = payload.reason;
|
||||
// If the server lost our session (race condition), re-announce via hello
|
||||
// so the server creates a new session and we can retry auth.
|
||||
if (payload.reason === "not_paired" && hasClientSecret(this.clientState)) {
|
||||
this.sendHello();
|
||||
return;
|
||||
}
|
||||
this.phase = "auth_required";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user