mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-07 06:39:10 +08:00
147 lines
4.9 KiB
TypeScript
147 lines
4.9 KiB
TypeScript
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
import { z } from "zod";
|
|
|
|
export type Data = Record<string, any>;
|
|
export const token = () => randomBytes(24).toString("base64url");
|
|
export const equal = (a: string, b: string) => {
|
|
const left = Buffer.from(a),
|
|
right = Buffer.from(b);
|
|
return left.length === right.length && timingSafeEqual(left, right);
|
|
};
|
|
export class Failure extends Error {
|
|
constructor(
|
|
public code: string,
|
|
public sideEffect = false,
|
|
public retryable = false,
|
|
public reason?: string,
|
|
) {
|
|
super(code);
|
|
}
|
|
}
|
|
export function failure(error: unknown): Data {
|
|
const e = error instanceof Failure ? error : new Failure("INTERNAL_ERROR");
|
|
return {
|
|
code: e.code,
|
|
retryable: e.retryable,
|
|
side_effect_possible: e.sideEffect,
|
|
...(e.reason ? { reason: e.reason } : {}),
|
|
};
|
|
}
|
|
export function parse<T extends z.ZodType>(
|
|
schema: T,
|
|
value: unknown,
|
|
): z.output<T> {
|
|
const result = schema.safeParse(value);
|
|
if (!result.success) throw new Failure("INVALID_ARGUMENT");
|
|
return result.data;
|
|
}
|
|
export const id = z
|
|
.string()
|
|
.min(1)
|
|
.max(128)
|
|
.regex(/^[a-zA-Z0-9_-]+$/);
|
|
export const alias = z
|
|
.string()
|
|
.min(1)
|
|
.max(80)
|
|
.regex(/^[\p{L}\p{N} _.-]+$/u);
|
|
export const mode = z.enum(["observe", "control"]);
|
|
export const unlockPassword = z.string().min(1).max(256)
|
|
.refine((value) => Buffer.byteLength(value) <= 256 && !/[\0\r\n\t]/.test(value));
|
|
export const panel = z.enum(["auto", "apps", "browser", "terminal", "off"]);
|
|
export type Panel = z.infer<typeof panel>;
|
|
export const videoOptions = z.strictObject({
|
|
decode_path: z.enum(["browser", "minirtc"]).default("browser"),
|
|
decoder: z.enum(["auto", "software"]).default("auto"),
|
|
});
|
|
export type VideoOptions = z.infer<typeof videoOptions>;
|
|
export const host = z
|
|
.string()
|
|
.min(1)
|
|
.max(253)
|
|
.regex(/^[a-zA-Z0-9.:-]+$/);
|
|
export const point = z.strictObject({
|
|
x: z.number().finite().min(0).max(8192),
|
|
y: z.number().finite().min(0).max(8192),
|
|
});
|
|
const button = z.enum(["left", "right", "middle"]).optional();
|
|
const unique = (keys: string[]) => new Set(keys).size === keys.length;
|
|
const modifiers = z.array(z.enum(["CTRL", "ALT", "SHIFT", "META"])).max(4).refine(unique)
|
|
.describe("Modifiers held during this action and released afterwards, including on cancellation.").optional();
|
|
const key = z.enum([
|
|
..."ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split(""),
|
|
"BACKSPACE", "TAB", "ENTER", "SHIFT", "CTRL", "ALT", "PAUSE", "CAPS_LOCK",
|
|
"ESC", "SPACE", "PAGE_UP", "PAGE_DOWN", "END", "HOME", "LEFT", "UP", "RIGHT", "DOWN",
|
|
"INSERT", "DELETE", "META", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12",
|
|
"SEMICOLON", "EQUAL", "COMMA", "MINUS", "PERIOD", "SLASH", "BACKQUOTE", "BRACKET_LEFT", "BACKSLASH", "BRACKET_RIGHT", "QUOTE",
|
|
]);
|
|
export const action = z.discriminatedUnion("type", [
|
|
point.extend({ type: z.literal("move"), modifiers }),
|
|
point.extend({ type: z.literal("click"), button, modifiers }),
|
|
point.extend({ type: z.literal("double_click"), button, modifiers }),
|
|
point.extend({
|
|
type: z.literal("scroll"),
|
|
direction: z.enum(["up", "down", "left", "right"]),
|
|
ticks: z.int().min(1).max(10).optional(),
|
|
modifiers,
|
|
}),
|
|
z.strictObject({
|
|
type: z.literal("drag"),
|
|
path: z.array(point).min(2).max(128),
|
|
duration_ms: z.int().min(100).max(2000).optional(),
|
|
button,
|
|
modifiers,
|
|
}),
|
|
z.strictObject({ type: z.literal("key_press"), key, modifiers })
|
|
.refine((p) => !p.modifiers?.some((modifier) => modifier === p.key)),
|
|
z.strictObject({
|
|
type: z.literal("hotkey"),
|
|
keys: z.array(key).min(2).max(6).refine(unique),
|
|
}),
|
|
z.strictObject({
|
|
type: z.literal("type_text"),
|
|
text: z
|
|
.string()
|
|
.min(1)
|
|
.max(16384)
|
|
.refine((s) => Buffer.byteLength(s) <= 16384 && !s.includes("\0")),
|
|
}),
|
|
]);
|
|
export const screenshot = z.strictObject({
|
|
session_id: id,
|
|
region: z
|
|
.strictObject({
|
|
x: z.int().min(0),
|
|
y: z.int().min(0),
|
|
width: z.int().min(1).max(8192),
|
|
height: z.int().min(1).max(8192),
|
|
})
|
|
.optional(),
|
|
max_edge: z.int().min(64).max(2048).optional(),
|
|
after_frame_id: z.int().min(0).optional(),
|
|
generation: z.int().min(0).optional(),
|
|
timeout_ms: z.int().min(0).max(5000).optional(),
|
|
});
|
|
export const uiConnect = z.strictObject({
|
|
device_alias: alias.optional(),
|
|
device_id: z
|
|
.string()
|
|
.regex(/^\d{9}$/)
|
|
.optional(),
|
|
label: alias.optional(),
|
|
password: z
|
|
.string()
|
|
.max(256)
|
|
.refine((s) => Buffer.byteLength(s) <= 256 && !/[\0\r\n]/.test(s))
|
|
.optional(),
|
|
mode: mode.default("observe"),
|
|
display_index: z.int().min(0).max(63).default(0),
|
|
server: host.optional(),
|
|
port: z.int().min(1).max(65535).optional(),
|
|
save_device: z.boolean().default(false),
|
|
remember_password: z.boolean().default(false),
|
|
unlock_password: unlockPassword.optional(),
|
|
remember_unlock_password: z.boolean().default(false),
|
|
persistent_authorization: z.boolean().default(false),
|
|
});
|