From cec59784de4cd1f0a9643512cd233bb8ebfa5ccb Mon Sep 17 00:00:00 2001 From: nav Date: Wed, 8 Apr 2026 21:38:43 +0000 Subject: [PATCH] feat: handle client pairing messages --- plugin/core/runtime.ts | 104 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/plugin/core/runtime.ts b/plugin/core/runtime.ts index 0aa484c..ccab68a 100644 --- a/plugin/core/runtime.ts +++ b/plugin/core/runtime.ts @@ -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 { + 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(