[WIP] A simple wake-on-lan service
1import z from "zod";
2
3function route<T, I>(opts: {
4 route: `/${string}`;
5 output: z.ZodType<T>;
6 method: "GET";
7 input: z.ZodType<I>;
8}): (data: I) => Promise<T>;
9function route<T, I>(opts: {
10 route: `/${string}`;
11 output: z.ZodType<T>;
12 method: "POST";
13 input: z.ZodType<I>;
14}): (data: I) => Promise<T>;
15function route<T, I>(opts: {
16 route: `/${string}`;
17 output: z.ZodType<T>;
18 method: "GET";
19}): () => Promise<T>;
20function route<T, I>(opts: {
21 route: `/${string}`;
22 output: z.ZodType<T>;
23 method: "POST";
24}): () => Promise<T>;
25function route<T, I>({
26 route,
27 output,
28 method,
29 input,
30}: {
31 route: `/${string}`;
32 output: z.ZodType<T>;
33 method: "GET" | "POST";
34 input?: z.ZodType<I>;
35}): ((data: I) => Promise<T>) | (() => Promise<T>) {
36 const stringify =
37 method == "GET"
38 ? (obj: I) =>
39 new URLSearchParams(
40 Object.fromEntries(
41 Object.entries(obj as Object).map(([k, v]) => [k, "" + v]),
42 ),
43 ).toString()
44 : (obj: I) => JSON.stringify(obj);
45
46 const then = (req: Response) => req.json().then(output.parseAsync);
47
48 return input
49 ? method == "GET"
50 ? (data: I) => fetch(route + "?" + stringify(data), { method }).then(then)
51 : (data: I) =>
52 fetch(route, {
53 method,
54 body: stringify(data),
55 headers: { "Content-Type": "application/json" },
56 }).then(then)
57 : () => fetch(route, { method }).then(then);
58}
59
60const u8 = z.int().min(0).max(255);
61const colour = z.tuple([u8, u8, u8]);
62
63const Api = {
64 config: route({
65 route: "/config",
66 method: "GET",
67 output: z.object({
68 binding: z.string(),
69 theme: z.object({
70 background: colour,
71 background_main: colour,
72 background_server: colour,
73 background_button: colour,
74 text: colour,
75 text_secondary: colour,
76 accent_success: colour,
77 accent_fail: colour,
78 link: colour,
79 link_visited: colour,
80 highlight: colour,
81 highlight_opacity: u8,
82 fonts: z.array(z.string()),
83 }),
84 info: z.object({
85 title: z.string(),
86 links: z.array(z.tuple([z.string(), z.string()])),
87 icon: z.string(),
88 }),
89 pinned: z.array(z.string()),
90 targets: z.record(
91 z.string(),
92 z.object({
93 mac: z.string(),
94 ip: z.string().nullable(),
95 // should be a url but we allow any string in rust
96 url: z.string().nullable(),
97 }),
98 ),
99 }),
100 }),
101
102 wake: route({
103 route: "/wake",
104 method: "POST",
105 input: z.object({
106 mac: z.string(),
107 }),
108 output: z.boolean(),
109 }),
110
111 ping: route({
112 route: "/ping",
113 method: "GET",
114 input: z.object({
115 ip: z.string(),
116 }),
117 output: z.boolean(),
118 }),
119} as const;
120
121export const config = await Api.config();
122
123export default Api;