From de9c41fc88006a9fdbba07882e2e1d116ac4fc3a Mon Sep 17 00:00:00 2001 From: root Date: Wed, 8 Apr 2026 20:03:28 +0000 Subject: [PATCH] feat: add shared protocol types --- README.md | 3 +- src/index.ts | 1 + src/types.ts | 182 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/index.ts create mode 100644 src/types.ts diff --git a/README.md b/README.md index 7350721..1dd01cf 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ It is referenced as a git submodule by both `Yonexus.Server` and `Yonexus.Client ## Contents - `PROTOCOL.md` — full protocol specification -- TypeScript type definitions (planned: `src/types.ts`) +- `src/types.ts` — shared builtin envelope / payload TypeScript definitions +- `src/index.ts` — package export surface - Canonical JSON shape references ## Purpose diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..a0c4ebf --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export * from "./types.js"; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..ea4d261 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,182 @@ +export const YONEXUS_PROTOCOL_VERSION = "1" as const; + +export const builtinMessageTypes = [ + "hello", + "hello_ack", + "pair_request", + "pair_confirm", + "pair_success", + "pair_failed", + "auth_request", + "auth_success", + "auth_failed", + "re_pair_required", + "heartbeat", + "heartbeat_ack", + "status_update", + "disconnect_notice", + "error" +] as const; + +export type BuiltinMessageType = (typeof builtinMessageTypes)[number]; + +export interface BuiltinEnvelope< + TType extends BuiltinMessageType = BuiltinMessageType, + TPayload extends Record = Record +> { + type: TType; + requestId?: string; + timestamp?: number; + payload?: TPayload; +} + +export interface HelloPayload { + identifier: string; + hasSecret: boolean; + hasKeyPair: boolean; + publicKey?: string; + protocolVersion: string; +} + +export type HelloAckNextAction = + | "pair_required" + | "auth_required" + | "rejected" + | "waiting_pair_confirm"; + +export interface HelloAckPayload { + identifier: string; + nextAction: HelloAckNextAction; +} + +export type AdminNotificationStatus = "sent" | "failed"; + +export interface PairRequestPayload { + identifier: string; + expiresAt: number; + ttlSeconds: number; + adminNotification: AdminNotificationStatus; + codeDelivery: "out_of_band"; +} + +export interface PairConfirmPayload { + identifier: string; + pairingCode: string; +} + +export interface PairSuccessPayload { + identifier: string; + secret: string; + pairedAt: number; +} + +export type PairFailedReason = + | "expired" + | "invalid_code" + | "identifier_not_allowed" + | "admin_notification_failed" + | "internal_error"; + +export interface PairFailedPayload { + identifier: string; + reason: PairFailedReason; +} + +export interface AuthRequestPayload { + identifier: string; + nonce: string; + proofTimestamp: number; + signature: string; + publicKey?: string; +} + +export interface AuthSuccessPayload { + identifier: string; + authenticatedAt: number; + status: "online"; +} + +export type AuthFailedReason = + | "unknown_identifier" + | "not_paired" + | "invalid_signature" + | "invalid_secret" + | "stale_timestamp" + | "future_timestamp" + | "nonce_collision" + | "rate_limited" + | "re_pair_required"; + +export interface AuthFailedPayload { + identifier: string; + reason: AuthFailedReason; +} + +export interface RePairRequiredPayload { + identifier: string; + reason: "nonce_collision" | "rate_limited" | "trust_revoked"; +} + +export interface HeartbeatPayload { + identifier: string; + status: "alive"; +} + +export interface HeartbeatAckPayload { + identifier: string; + status: "online" | "unstable" | "offline"; +} + +export interface StatusUpdatePayload { + identifier: string; + status: "online" | "unstable" | "offline"; + reason: string; +} + +export interface DisconnectNoticePayload { + identifier: string; + reason: string; +} + +export type ProtocolErrorCode = + | "MALFORMED_MESSAGE" + | "UNSUPPORTED_PROTOCOL_VERSION" + | "IDENTIFIER_NOT_ALLOWED" + | "PAIRING_REQUIRED" + | "PAIRING_EXPIRED" + | "ADMIN_NOTIFICATION_FAILED" + | "AUTH_FAILED" + | "NONCE_COLLISION" + | "RATE_LIMITED" + | "RE_PAIR_REQUIRED" + | "CLIENT_OFFLINE" + | "INTERNAL_ERROR"; + +export interface ErrorPayload { + code: ProtocolErrorCode; + message?: string; + details?: Record; +} + +export type BuiltinPayloadMap = { + hello: HelloPayload; + hello_ack: HelloAckPayload; + pair_request: PairRequestPayload; + pair_confirm: PairConfirmPayload; + pair_success: PairSuccessPayload; + pair_failed: PairFailedPayload; + auth_request: AuthRequestPayload; + auth_success: AuthSuccessPayload; + auth_failed: AuthFailedPayload; + re_pair_required: RePairRequiredPayload; + heartbeat: HeartbeatPayload; + heartbeat_ack: HeartbeatAckPayload; + status_update: StatusUpdatePayload; + disconnect_notice: DisconnectNoticePayload; + error: ErrorPayload; +}; + +export type TypedBuiltinEnvelope = BuiltinEnvelope< + TType, + BuiltinPayloadMap[TType] +>;