feat: handle client pairing messages

This commit is contained in:
nav
2026-04-08 21:38:43 +00:00
parent fc226b1f18
commit cec59784de

View File

@@ -1,10 +1,14 @@
import {
YONEXUS_PROTOCOL_VERSION,
buildHello,
buildPairConfirm,
decodeBuiltin,
encodeBuiltin,
isBuiltinMessage,
type HelloAckPayload,
type PairFailedPayload,
type PairRequestPayload,
type PairSuccessPayload,
type TypedBuiltinEnvelope
} from "../../../Yonexus.Protocol/src/index.js";
import type { YonexusClientConfig } from "./config.js";
@@ -39,6 +43,12 @@ export interface YonexusClientRuntimeState {
readonly phase: YonexusClientPhase;
readonly transportState: ClientConnectionState;
readonly clientState: YonexusClientState;
readonly pendingPairing?: {
expiresAt: number;
ttlSeconds: number;
adminNotification: "sent" | "failed";
};
readonly lastPairingFailure?: string;
}
export class YonexusClientRuntime {
@@ -46,6 +56,12 @@ export class YonexusClientRuntime {
private readonly now: () => number;
private clientState: YonexusClientState;
private phase: YonexusClientPhase = "idle";
private pendingPairing?: {
expiresAt: number;
ttlSeconds: number;
adminNotification: "sent" | "failed";
};
private lastPairingFailure?: string;
constructor(options: YonexusClientRuntimeOptions) {
this.options = options;
@@ -57,7 +73,9 @@ export class YonexusClientRuntime {
return {
phase: this.phase,
transportState: this.options.transport.state,
clientState: this.clientState
clientState: this.clientState,
pendingPairing: this.pendingPairing,
lastPairingFailure: this.lastPairingFailure
};
}
@@ -100,6 +118,21 @@ export class YonexusClientRuntime {
return;
}
if (envelope.type === "pair_request") {
this.handlePairRequest(envelope as TypedBuiltinEnvelope<"pair_request">);
return;
}
if (envelope.type === "pair_success") {
await this.handlePairSuccess(envelope as TypedBuiltinEnvelope<"pair_success">);
return;
}
if (envelope.type === "pair_failed") {
this.handlePairFailed(envelope as TypedBuiltinEnvelope<"pair_failed">);
return;
}
if (envelope.type === "auth_success") {
this.phase = "authenticated";
return;
@@ -134,6 +167,26 @@ export class YonexusClientRuntime {
);
}
submitPairingCode(pairingCode: string, requestId?: string): boolean {
const normalizedCode = pairingCode.trim();
if (!normalizedCode || !this.options.transport.isConnected) {
return false;
}
this.lastPairingFailure = undefined;
return this.options.transport.send(
encodeBuiltin(
buildPairConfirm(
{
identifier: this.options.config.identifier,
pairingCode: normalizedCode
},
{ requestId, timestamp: this.now() }
)
)
);
}
private handleHelloAck(envelope: TypedBuiltinEnvelope<"hello_ack">): void {
const payload = envelope.payload as HelloAckPayload | undefined;
if (!payload) {
@@ -155,6 +208,55 @@ export class YonexusClientRuntime {
break;
}
}
private handlePairRequest(envelope: TypedBuiltinEnvelope<"pair_request">): void {
const payload = envelope.payload as PairRequestPayload | undefined;
if (!payload) {
return;
}
this.pendingPairing = {
expiresAt: payload.expiresAt,
ttlSeconds: payload.ttlSeconds,
adminNotification: payload.adminNotification
};
this.lastPairingFailure = undefined;
this.phase = payload.adminNotification === "sent" ? "waiting_pair_confirm" : "pair_required";
}
private async handlePairSuccess(envelope: TypedBuiltinEnvelope<"pair_success">): Promise<void> {
const payload = envelope.payload as PairSuccessPayload | undefined;
if (!payload) {
return;
}
this.clientState = {
...this.clientState,
secret: payload.secret,
pairedAt: payload.pairedAt,
updatedAt: this.now()
};
await this.options.stateStore.save(this.clientState);
this.pendingPairing = undefined;
this.lastPairingFailure = undefined;
this.phase = "auth_required";
}
private handlePairFailed(envelope: TypedBuiltinEnvelope<"pair_failed">): void {
const payload = envelope.payload as PairFailedPayload | undefined;
if (!payload) {
return;
}
this.lastPairingFailure = payload.reason;
if (payload.reason === "expired" || payload.reason === "admin_notification_failed") {
this.pendingPairing = undefined;
this.phase = "pair_required";
return;
}
this.phase = "waiting_pair_confirm";
}
}
export function createYonexusClientRuntime(