Primo commit - Prompt 02 — WebSocket endpoints /agent e /client + parsing robusto
This commit is contained in:
14
shared/protocol/package.json
Normal file
14
shared/protocol/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@assistenza/protocol",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
210
shared/protocol/src/index.ts
Normal file
210
shared/protocol/src/index.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
Shared protocol v1 types and helpers.
|
||||
Keep validation minimal and dependency-free.
|
||||
*/
|
||||
|
||||
export const ProtocolVersion = 1 as const;
|
||||
export type ProtocolVersion = typeof ProtocolVersion;
|
||||
|
||||
export type RequestId = string;
|
||||
|
||||
export type MessageBase = {
|
||||
v: ProtocolVersion;
|
||||
type: string;
|
||||
requestId: RequestId;
|
||||
};
|
||||
|
||||
// Agent -> Server
|
||||
export type AgentRegister = MessageBase & {
|
||||
type: "agent_register";
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
pairingKey: string;
|
||||
};
|
||||
|
||||
export type AgentRegistered = MessageBase & {
|
||||
type: "agent_registered";
|
||||
deviceId: string;
|
||||
};
|
||||
|
||||
export type AgentHeartbeat = MessageBase & {
|
||||
type: "agent_heartbeat";
|
||||
deviceId: string;
|
||||
ts: number;
|
||||
};
|
||||
|
||||
// Server -> Agent
|
||||
export type ServerConnectRequest = MessageBase & {
|
||||
type: "server_connect_request";
|
||||
deviceId: string;
|
||||
sessionId: string;
|
||||
clientId: string;
|
||||
};
|
||||
|
||||
export type AgentConnectResponse = MessageBase & {
|
||||
type: "agent_connect_response";
|
||||
deviceId: string;
|
||||
sessionId: string;
|
||||
accepted: boolean;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
// Client -> Server
|
||||
export type ClientLogin = MessageBase & {
|
||||
type: "client_login";
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type ClientLoginResult = MessageBase & {
|
||||
type: "client_login_result";
|
||||
ok: boolean;
|
||||
clientId?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export type ClientListDevices = MessageBase & {
|
||||
type: "client_list_devices";
|
||||
};
|
||||
|
||||
export type ClientDeviceList = MessageBase & {
|
||||
type: "client_device_list";
|
||||
devices: Array<{
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
online: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ClientConnectRequest = MessageBase & {
|
||||
type: "client_connect_request";
|
||||
deviceId: string;
|
||||
};
|
||||
|
||||
export type ClientConnectStatus = MessageBase & {
|
||||
type: "client_connect_status";
|
||||
deviceId: string;
|
||||
sessionId: string;
|
||||
status: "pending" | "accepted" | "denied" | "expired";
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
export type ClientConnectResult = MessageBase & {
|
||||
type: "client_connect_result";
|
||||
deviceId: string;
|
||||
sessionId: string;
|
||||
ok: boolean;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
// Error
|
||||
export type ErrorMessage = MessageBase & {
|
||||
type: "error";
|
||||
code: "UNAUTHORIZED" | "BAD_REQUEST" | "NOT_FOUND" | "CONFLICT" | "DEVICE_OFFLINE";
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type AnyMessage =
|
||||
| AgentRegister
|
||||
| AgentRegistered
|
||||
| AgentHeartbeat
|
||||
| ServerConnectRequest
|
||||
| AgentConnectResponse
|
||||
| ClientLogin
|
||||
| ClientLoginResult
|
||||
| ClientListDevices
|
||||
| ClientDeviceList
|
||||
| ClientConnectRequest
|
||||
| ClientConnectStatus
|
||||
| ClientConnectResult
|
||||
| ErrorMessage;
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function hasString(obj: Record<string, unknown>, key: string): boolean {
|
||||
return typeof obj[key] === "string" && obj[key] !== "";
|
||||
}
|
||||
|
||||
function hasNumber(obj: Record<string, unknown>, key: string): boolean {
|
||||
return typeof obj[key] === "number" && Number.isFinite(obj[key]);
|
||||
}
|
||||
|
||||
export function isMessage(obj: unknown): obj is AnyMessage {
|
||||
if (!isObject(obj)) return false;
|
||||
if (obj.v !== ProtocolVersion) return false;
|
||||
if (!hasString(obj, "type")) return false;
|
||||
if (!hasString(obj, "requestId")) return false;
|
||||
|
||||
switch (obj.type) {
|
||||
case "agent_register":
|
||||
return hasString(obj, "deviceId") && hasString(obj, "deviceName") && hasString(obj, "pairingKey");
|
||||
case "agent_registered":
|
||||
return hasString(obj, "deviceId");
|
||||
case "agent_heartbeat":
|
||||
return hasString(obj, "deviceId") && hasNumber(obj, "ts");
|
||||
case "server_connect_request":
|
||||
return hasString(obj, "deviceId") && hasString(obj, "sessionId") && hasString(obj, "clientId");
|
||||
case "agent_connect_response":
|
||||
return (
|
||||
hasString(obj, "deviceId") &&
|
||||
hasString(obj, "sessionId") &&
|
||||
typeof obj.accepted === "boolean"
|
||||
);
|
||||
case "client_login":
|
||||
return hasString(obj, "username") && hasString(obj, "password");
|
||||
case "client_login_result":
|
||||
return typeof obj.ok === "boolean";
|
||||
case "client_list_devices":
|
||||
return true;
|
||||
case "client_device_list":
|
||||
return Array.isArray(obj.devices);
|
||||
case "client_connect_request":
|
||||
return hasString(obj, "deviceId");
|
||||
case "client_connect_status":
|
||||
return (
|
||||
hasString(obj, "deviceId") &&
|
||||
hasString(obj, "sessionId") &&
|
||||
hasString(obj, "status")
|
||||
);
|
||||
case "client_connect_result":
|
||||
return (
|
||||
hasString(obj, "deviceId") &&
|
||||
hasString(obj, "sessionId") &&
|
||||
typeof obj.ok === "boolean"
|
||||
);
|
||||
case "error":
|
||||
return hasString(obj, "code") && hasString(obj, "message");
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function nowTs(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
// Minimal UUID v4 (RFC4122) generator without dependencies.
|
||||
export function makeRequestId(): string {
|
||||
const bytes = new Uint8Array(16);
|
||||
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
||||
crypto.getRandomValues(bytes);
|
||||
} else {
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
bytes[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
}
|
||||
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
|
||||
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
|
||||
return (
|
||||
hex.slice(0, 4).join("") + "-" +
|
||||
hex.slice(4, 6).join("") + "-" +
|
||||
hex.slice(6, 8).join("") + "-" +
|
||||
hex.slice(8, 10).join("") + "-" +
|
||||
hex.slice(10, 16).join("")
|
||||
);
|
||||
}
|
||||
8
shared/protocol/tsconfig.json
Normal file
8
shared/protocol/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user