feat(build): integrate WASM build and portal frontend serving

- Add WASM build with optimization using binaryen in Dockerfile and Makefile - Update .gitignore to exclude dist/, *.wasm, and docker-compose overrides - Refactor Dockerfile to build WASM and server separately, copy static dist files, and set portal domain env var - Simplify view.go by removing unused imports and adding serveAsset helper for static file serving - Enable relay server to serve portal frontend on port 4017 with embedded assets This enhances the build process for better performance and adds web portal functionality to the relay server.

lemon-mint committed Nov 4, 2025 at 12:07 UTC bb8a5b240e9f5d74d0360eb18ec773ed673dc46c
15 files changed +987 -1551
.dockerignore new
+3
@@ -0,0 +1,3 @@
1 +dist/
2 +bin/
3 +docker-compose.override.yaml
.gitignore
+6
@@ -8,10 +8,12 @@
8 *.so
9 *.dylib
10 bin/
11 +dist/
12 chat
13
14 # Test binary, built with `go test -c`
15 *.test
16 +*.wasm
17
18 # Code coverage profiles and other test artifacts
19 *.out
@@ -30,3 +32,7 @@ profile.cov
32 # .vscode/
33
34 /target
35 +
36 +# Docker Compose Override
37 +docker-compose.override.yml
38 +docker-compose.override.yaml
Cargo.lock deleted
-1233
@@ -1,1233 +0,0 @@
1 -# This file is automatically @generated by Cargo.
2 -# It is not intended for manual editing.
3 -version = 4
4 -
5 -[[package]]
6 -name = "aead"
7 -version = "0.5.2"
8 -source = "registry+https://github.com/rust-lang/crates.io-index"
9 -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
10 -dependencies = [
11 - "crypto-common",
12 - "generic-array",
13 -]
14 -
15 -[[package]]
16 -name = "aho-corasick"
17 -version = "1.1.3"
18 -source = "registry+https://github.com/rust-lang/crates.io-index"
19 -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
20 -dependencies = [
21 - "memchr",
22 -]
23 -
24 -[[package]]
25 -name = "anyhow"
26 -version = "1.0.100"
27 -source = "registry+https://github.com/rust-lang/crates.io-index"
28 -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
29 -
30 -[[package]]
31 -name = "base64ct"
32 -version = "1.8.0"
33 -source = "registry+https://github.com/rust-lang/crates.io-index"
34 -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
35 -
36 -[[package]]
37 -name = "bitflags"
38 -version = "2.10.0"
39 -source = "registry+https://github.com/rust-lang/crates.io-index"
40 -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
41 -
42 -[[package]]
43 -name = "block-buffer"
44 -version = "0.10.4"
45 -source = "registry+https://github.com/rust-lang/crates.io-index"
46 -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
47 -dependencies = [
48 - "generic-array",
49 -]
50 -
51 -[[package]]
52 -name = "bumpalo"
53 -version = "3.19.0"
54 -source = "registry+https://github.com/rust-lang/crates.io-index"
55 -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
56 -
57 -[[package]]
58 -name = "bytes"
59 -version = "1.10.1"
60 -source = "registry+https://github.com/rust-lang/crates.io-index"
61 -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
62 -
63 -[[package]]
64 -name = "cc"
65 -version = "1.2.43"
66 -source = "registry+https://github.com/rust-lang/crates.io-index"
67 -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2"
68 -dependencies = [
69 - "find-msvc-tools",
70 - "shlex",
71 -]
72 -
73 -[[package]]
74 -name = "cfg-if"
75 -version = "0.1.10"
76 -source = "registry+https://github.com/rust-lang/crates.io-index"
77 -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
78 -
79 -[[package]]
80 -name = "cfg-if"
81 -version = "1.0.4"
82 -source = "registry+https://github.com/rust-lang/crates.io-index"
83 -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
84 -
85 -[[package]]
86 -name = "chacha20"
87 -version = "0.9.1"
88 -source = "registry+https://github.com/rust-lang/crates.io-index"
89 -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
90 -dependencies = [
91 - "cfg-if 1.0.4",
92 - "cipher",
93 - "cpufeatures",
94 -]
95 -
96 -[[package]]
97 -name = "chacha20poly1305"
98 -version = "0.10.1"
99 -source = "registry+https://github.com/rust-lang/crates.io-index"
100 -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
101 -dependencies = [
102 - "aead",
103 - "chacha20",
104 - "cipher",
105 - "poly1305",
106 - "zeroize",
107 -]
108 -
109 -[[package]]
110 -name = "cipher"
111 -version = "0.4.4"
112 -source = "registry+https://github.com/rust-lang/crates.io-index"
113 -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
114 -dependencies = [
115 - "crypto-common",
116 - "inout",
117 - "zeroize",
118 -]
119 -
120 -[[package]]
121 -name = "console_error_panic_hook"
122 -version = "0.1.7"
123 -source = "registry+https://github.com/rust-lang/crates.io-index"
124 -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
125 -dependencies = [
126 - "cfg-if 1.0.4",
127 - "wasm-bindgen",
128 -]
129 -
130 -[[package]]
131 -name = "const-oid"
132 -version = "0.9.6"
133 -source = "registry+https://github.com/rust-lang/crates.io-index"
134 -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
135 -
136 -[[package]]
137 -name = "cpufeatures"
138 -version = "0.2.17"
139 -source = "registry+https://github.com/rust-lang/crates.io-index"
140 -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
141 -dependencies = [
142 - "libc",
143 -]
144 -
145 -[[package]]
146 -name = "crypto-common"
147 -version = "0.1.6"
148 -source = "registry+https://github.com/rust-lang/crates.io-index"
149 -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
150 -dependencies = [
151 - "generic-array",
152 - "rand_core",
153 - "typenum",
154 -]
155 -
156 -[[package]]
157 -name = "curve25519-dalek"
158 -version = "4.1.3"
159 -source = "registry+https://github.com/rust-lang/crates.io-index"
160 -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
161 -dependencies = [
162 - "cfg-if 1.0.4",
163 - "cpufeatures",
164 - "curve25519-dalek-derive",
165 - "digest",
166 - "fiat-crypto",
167 - "rustc_version",
168 - "subtle",
169 - "zeroize",
170 -]
171 -
172 -[[package]]
173 -name = "curve25519-dalek-derive"
174 -version = "0.1.1"
175 -source = "registry+https://github.com/rust-lang/crates.io-index"
176 -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
177 -dependencies = [
178 - "proc-macro2",
179 - "quote",
180 - "syn",
181 -]
182 -
183 -[[package]]
184 -name = "der"
185 -version = "0.7.10"
186 -source = "registry+https://github.com/rust-lang/crates.io-index"
187 -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
188 -dependencies = [
189 - "const-oid",
190 - "zeroize",
191 -]
192 -
193 -[[package]]
194 -name = "digest"
195 -version = "0.10.7"
196 -source = "registry+https://github.com/rust-lang/crates.io-index"
197 -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
198 -dependencies = [
199 - "block-buffer",
200 - "crypto-common",
201 - "subtle",
202 -]
203 -
204 -[[package]]
205 -name = "ed25519"
206 -version = "2.2.3"
207 -source = "registry+https://github.com/rust-lang/crates.io-index"
208 -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
209 -dependencies = [
210 - "pkcs8",
211 - "signature",
212 -]
213 -
214 -[[package]]
215 -name = "ed25519-dalek"
216 -version = "2.2.0"
217 -source = "registry+https://github.com/rust-lang/crates.io-index"
218 -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
219 -dependencies = [
220 - "curve25519-dalek",
221 - "ed25519",
222 - "rand_core",
223 - "serde",
224 - "sha2",
225 - "subtle",
226 - "zeroize",
227 -]
228 -
229 -[[package]]
230 -name = "either"
231 -version = "1.15.0"
232 -source = "registry+https://github.com/rust-lang/crates.io-index"
233 -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
234 -
235 -[[package]]
236 -name = "equivalent"
237 -version = "1.0.2"
238 -source = "registry+https://github.com/rust-lang/crates.io-index"
239 -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
240 -
241 -[[package]]
242 -name = "errno"
243 -version = "0.3.14"
244 -source = "registry+https://github.com/rust-lang/crates.io-index"
245 -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
246 -dependencies = [
247 - "libc",
248 - "windows-sys",
249 -]
250 -
251 -[[package]]
252 -name = "fastrand"
253 -version = "2.3.0"
254 -source = "registry+https://github.com/rust-lang/crates.io-index"
255 -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
256 -
257 -[[package]]
258 -name = "fiat-crypto"
259 -version = "0.2.9"
260 -source = "registry+https://github.com/rust-lang/crates.io-index"
261 -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
262 -
263 -[[package]]
264 -name = "find-msvc-tools"
265 -version = "0.1.4"
266 -source = "registry+https://github.com/rust-lang/crates.io-index"
267 -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
268 -
269 -[[package]]
270 -name = "fixedbitset"
271 -version = "0.5.7"
272 -source = "registry+https://github.com/rust-lang/crates.io-index"
273 -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
274 -
275 -[[package]]
276 -name = "futures"
277 -version = "0.3.31"
278 -source = "registry+https://github.com/rust-lang/crates.io-index"
279 -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
280 -dependencies = [
281 - "futures-channel",
282 - "futures-core",
283 - "futures-io",
284 - "futures-sink",
285 - "futures-task",
286 - "futures-util",
287 -]
288 -
289 -[[package]]
290 -name = "futures-channel"
291 -version = "0.3.31"
292 -source = "registry+https://github.com/rust-lang/crates.io-index"
293 -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
294 -dependencies = [
295 - "futures-core",
296 - "futures-sink",
297 -]
298 -
299 -[[package]]
300 -name = "futures-core"
301 -version = "0.3.31"
302 -source = "registry+https://github.com/rust-lang/crates.io-index"
303 -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
304 -
305 -[[package]]
306 -name = "futures-io"
307 -version = "0.3.31"
308 -source = "registry+https://github.com/rust-lang/crates.io-index"
309 -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
310 -
311 -[[package]]
312 -name = "futures-macro"
313 -version = "0.3.31"
314 -source = "registry+https://github.com/rust-lang/crates.io-index"
315 -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
316 -dependencies = [
317 - "proc-macro2",
318 - "quote",
319 - "syn",
320 -]
321 -
322 -[[package]]
323 -name = "futures-sink"
324 -version = "0.3.31"
325 -source = "registry+https://github.com/rust-lang/crates.io-index"
326 -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
327 -
328 -[[package]]
329 -name = "futures-task"
330 -version = "0.3.31"
331 -source = "registry+https://github.com/rust-lang/crates.io-index"
332 -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
333 -
334 -[[package]]
335 -name = "futures-util"
336 -version = "0.3.31"
337 -source = "registry+https://github.com/rust-lang/crates.io-index"
338 -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
339 -dependencies = [
340 - "futures-channel",
341 - "futures-core",
342 - "futures-io",
343 - "futures-macro",
344 - "futures-sink",
345 - "futures-task",
346 - "memchr",
347 - "pin-project-lite",
348 - "pin-utils",
349 - "slab",
350 -]
351 -
352 -[[package]]
353 -name = "generic-array"
354 -version = "0.14.9"
355 -source = "registry+https://github.com/rust-lang/crates.io-index"
356 -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
357 -dependencies = [
358 - "typenum",
359 - "version_check",
360 -]
361 -
362 -[[package]]
363 -name = "getrandom"
364 -version = "0.2.16"
365 -source = "registry+https://github.com/rust-lang/crates.io-index"
366 -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
367 -dependencies = [
368 - "cfg-if 1.0.4",
369 - "js-sys",
370 - "libc",
371 - "wasi",
372 - "wasm-bindgen",
373 -]
374 -
375 -[[package]]
376 -name = "getrandom"
377 -version = "0.3.4"
378 -source = "registry+https://github.com/rust-lang/crates.io-index"
379 -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
380 -dependencies = [
381 - "cfg-if 1.0.4",
382 - "libc",
383 - "r-efi",
384 - "wasip2",
385 -]
386 -
387 -[[package]]
388 -name = "hashbrown"
389 -version = "0.16.0"
390 -source = "registry+https://github.com/rust-lang/crates.io-index"
391 -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
392 -
393 -[[package]]
394 -name = "heck"
395 -version = "0.5.0"
396 -source = "registry+https://github.com/rust-lang/crates.io-index"
397 -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
398 -
399 -[[package]]
400 -name = "hex"
401 -version = "0.4.3"
402 -source = "registry+https://github.com/rust-lang/crates.io-index"
403 -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
404 -
405 -[[package]]
406 -name = "hkdf"
407 -version = "0.12.4"
408 -source = "registry+https://github.com/rust-lang/crates.io-index"
409 -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
410 -dependencies = [
411 - "hmac",
412 -]
413 -
414 -[[package]]
415 -name = "hmac"
416 -version = "0.12.1"
417 -source = "registry+https://github.com/rust-lang/crates.io-index"
418 -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
419 -dependencies = [
420 - "digest",
421 -]
422 -
423 -[[package]]
424 -name = "indexmap"
425 -version = "2.12.0"
426 -source = "registry+https://github.com/rust-lang/crates.io-index"
427 -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
428 -dependencies = [
429 - "equivalent",
430 - "hashbrown",
431 -]
432 -
433 -[[package]]
434 -name = "inout"
435 -version = "0.1.4"
436 -source = "registry+https://github.com/rust-lang/crates.io-index"
437 -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
438 -dependencies = [
439 - "generic-array",
440 -]
441 -
442 -[[package]]
443 -name = "itertools"
444 -version = "0.14.0"
445 -source = "registry+https://github.com/rust-lang/crates.io-index"
446 -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
447 -dependencies = [
448 - "either",
449 -]
450 -
451 -[[package]]
452 -name = "itoa"
453 -version = "1.0.15"
454 -source = "registry+https://github.com/rust-lang/crates.io-index"
455 -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
456 -
457 -[[package]]
458 -name = "js-sys"
459 -version = "0.3.82"
460 -source = "registry+https://github.com/rust-lang/crates.io-index"
461 -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
462 -dependencies = [
463 - "once_cell",
464 - "wasm-bindgen",
465 -]
466 -
467 -[[package]]
468 -name = "libc"
469 -version = "0.2.177"
470 -source = "registry+https://github.com/rust-lang/crates.io-index"
471 -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
472 -
473 -[[package]]
474 -name = "linux-raw-sys"
475 -version = "0.11.0"
476 -source = "registry+https://github.com/rust-lang/crates.io-index"
477 -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
478 -
479 -[[package]]
480 -name = "lock_api"
481 -version = "0.4.14"
482 -source = "registry+https://github.com/rust-lang/crates.io-index"
483 -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
484 -dependencies = [
485 - "scopeguard",
486 -]
487 -
488 -[[package]]
489 -name = "log"
490 -version = "0.4.28"
491 -source = "registry+https://github.com/rust-lang/crates.io-index"
492 -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
493 -
494 -[[package]]
495 -name = "memchr"
496 -version = "2.7.6"
497 -source = "registry+https://github.com/rust-lang/crates.io-index"
498 -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
499 -
500 -[[package]]
501 -name = "memory_units"
502 -version = "0.4.0"
503 -source = "registry+https://github.com/rust-lang/crates.io-index"
504 -checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
505 -
506 -[[package]]
507 -name = "minicov"
508 -version = "0.3.7"
509 -source = "registry+https://github.com/rust-lang/crates.io-index"
510 -checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b"
511 -dependencies = [
512 - "cc",
513 - "walkdir",
514 -]
515 -
516 -[[package]]
517 -name = "multimap"
518 -version = "0.10.1"
519 -source = "registry+https://github.com/rust-lang/crates.io-index"
520 -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
521 -
522 -[[package]]
523 -name = "once_cell"
524 -version = "1.21.3"
525 -source = "registry+https://github.com/rust-lang/crates.io-index"
526 -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
527 -
528 -[[package]]
529 -name = "opaque-debug"
530 -version = "0.3.1"
531 -source = "registry+https://github.com/rust-lang/crates.io-index"
532 -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
533 -
534 -[[package]]
535 -name = "parking_lot"
536 -version = "0.12.5"
537 -source = "registry+https://github.com/rust-lang/crates.io-index"
538 -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
539 -dependencies = [
540 - "lock_api",
541 - "parking_lot_core",
542 -]
543 -
544 -[[package]]
545 -name = "parking_lot_core"
546 -version = "0.9.12"
547 -source = "registry+https://github.com/rust-lang/crates.io-index"
548 -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
549 -dependencies = [
550 - "cfg-if 1.0.4",
551 - "libc",
552 - "redox_syscall",
553 - "smallvec",
554 - "windows-link",
555 -]
556 -
557 -[[package]]
558 -name = "petgraph"
559 -version = "0.7.1"
560 -source = "registry+https://github.com/rust-lang/crates.io-index"
561 -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
562 -dependencies = [
563 - "fixedbitset",
564 - "indexmap",
565 -]
566 -
567 -[[package]]
568 -name = "pin-project-lite"
569 -version = "0.2.16"
570 -source = "registry+https://github.com/rust-lang/crates.io-index"
571 -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
572 -
573 -[[package]]
574 -name = "pin-utils"
575 -version = "0.1.0"
576 -source = "registry+https://github.com/rust-lang/crates.io-index"
577 -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
578 -
579 -[[package]]
580 -name = "pkcs8"
581 -version = "0.10.2"
582 -source = "registry+https://github.com/rust-lang/crates.io-index"
583 -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
584 -dependencies = [
585 - "der",
586 - "spki",
587 -]
588 -
589 -[[package]]
590 -name = "poly1305"
591 -version = "0.8.0"
592 -source = "registry+https://github.com/rust-lang/crates.io-index"
593 -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
594 -dependencies = [
595 - "cpufeatures",
596 - "opaque-debug",
597 - "universal-hash",
598 -]
599 -
600 -[[package]]
601 -name = "portal-wasm"
602 -version = "0.1.0"
603 -dependencies = [
604 - "anyhow",
605 - "bytes",
606 - "chacha20poly1305",
607 - "console_error_panic_hook",
608 - "ed25519-dalek",
609 - "futures",
610 - "getrandom 0.2.16",
611 - "hex",
612 - "hkdf",
613 - "js-sys",
614 - "log",
615 - "parking_lot",
616 - "prost",
617 - "prost-build",
618 - "rand_core",
619 - "serde",
620 - "serde-wasm-bindgen",
621 - "serde_json",
622 - "sha2",
623 - "thiserror",
624 - "tokio",
625 - "wasm-bindgen",
626 - "wasm-bindgen-futures",
627 - "wasm-bindgen-test",
628 - "web-sys",
629 - "wee_alloc",
630 - "x25519-dalek",
631 -]
632 -
633 -[[package]]
634 -name = "prettyplease"
635 -version = "0.2.37"
636 -source = "registry+https://github.com/rust-lang/crates.io-index"
637 -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
638 -dependencies = [
639 - "proc-macro2",
640 - "syn",
641 -]
642 -
643 -[[package]]
644 -name = "proc-macro2"
645 -version = "1.0.103"
646 -source = "registry+https://github.com/rust-lang/crates.io-index"
647 -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
648 -dependencies = [
649 - "unicode-ident",
650 -]
651 -
652 -[[package]]
653 -name = "prost"
654 -version = "0.14.1"
655 -source = "registry+https://github.com/rust-lang/crates.io-index"
656 -checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d"
657 -dependencies = [
658 - "bytes",
659 - "prost-derive",
660 -]
661 -
662 -[[package]]
663 -name = "prost-build"
664 -version = "0.14.1"
665 -source = "registry+https://github.com/rust-lang/crates.io-index"
666 -checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1"
667 -dependencies = [
668 - "heck",
669 - "itertools",
670 - "log",
671 - "multimap",
672 - "once_cell",
673 - "petgraph",
674 - "prettyplease",
675 - "prost",
676 - "prost-types",
677 - "regex",
678 - "syn",
679 - "tempfile",
680 -]
681 -
682 -[[package]]
683 -name = "prost-derive"
684 -version = "0.14.1"
685 -source = "registry+https://github.com/rust-lang/crates.io-index"
686 -checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
687 -dependencies = [
688 - "anyhow",
689 - "itertools",
690 - "proc-macro2",
691 - "quote",
692 - "syn",
693 -]
694 -
695 -[[package]]
696 -name = "prost-types"
697 -version = "0.14.1"
698 -source = "registry+https://github.com/rust-lang/crates.io-index"
699 -checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72"
700 -dependencies = [
701 - "prost",
702 -]
703 -
704 -[[package]]
705 -name = "quote"
706 -version = "1.0.41"
707 -source = "registry+https://github.com/rust-lang/crates.io-index"
708 -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
709 -dependencies = [
710 - "proc-macro2",
711 -]
712 -
713 -[[package]]
714 -name = "r-efi"
715 -version = "5.3.0"
716 -source = "registry+https://github.com/rust-lang/crates.io-index"
717 -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
718 -
719 -[[package]]
720 -name = "rand_core"
721 -version = "0.6.4"
722 -source = "registry+https://github.com/rust-lang/crates.io-index"
723 -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
724 -dependencies = [
725 - "getrandom 0.2.16",
726 -]
727 -
728 -[[package]]
729 -name = "redox_syscall"
730 -version = "0.5.18"
731 -source = "registry+https://github.com/rust-lang/crates.io-index"
732 -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
733 -dependencies = [
734 - "bitflags",
735 -]
736 -
737 -[[package]]
738 -name = "regex"
739 -version = "1.12.2"
740 -source = "registry+https://github.com/rust-lang/crates.io-index"
741 -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
742 -dependencies = [
743 - "aho-corasick",
744 - "memchr",
745 - "regex-automata",
746 - "regex-syntax",
747 -]
748 -
749 -[[package]]
750 -name = "regex-automata"
751 -version = "0.4.13"
752 -source = "registry+https://github.com/rust-lang/crates.io-index"
753 -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
754 -dependencies = [
755 - "aho-corasick",
756 - "memchr",
757 - "regex-syntax",
758 -]
759 -
760 -[[package]]
761 -name = "regex-syntax"
762 -version = "0.8.8"
763 -source = "registry+https://github.com/rust-lang/crates.io-index"
764 -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
765 -
766 -[[package]]
767 -name = "rustc_version"
768 -version = "0.4.1"
769 -source = "registry+https://github.com/rust-lang/crates.io-index"
770 -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
771 -dependencies = [
772 - "semver",
773 -]
774 -
775 -[[package]]
776 -name = "rustix"
777 -version = "1.1.2"
778 -source = "registry+https://github.com/rust-lang/crates.io-index"
779 -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
780 -dependencies = [
781 - "bitflags",
782 - "errno",
783 - "libc",
784 - "linux-raw-sys",
785 - "windows-sys",
786 -]
787 -
788 -[[package]]
789 -name = "rustversion"
790 -version = "1.0.22"
791 -source = "registry+https://github.com/rust-lang/crates.io-index"
792 -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
793 -
794 -[[package]]
795 -name = "ryu"
796 -version = "1.0.20"
797 -source = "registry+https://github.com/rust-lang/crates.io-index"
798 -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
799 -
800 -[[package]]
801 -name = "same-file"
802 -version = "1.0.6"
803 -source = "registry+https://github.com/rust-lang/crates.io-index"
804 -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
805 -dependencies = [
806 - "winapi-util",
807 -]
808 -
809 -[[package]]
810 -name = "scopeguard"
811 -version = "1.2.0"
812 -source = "registry+https://github.com/rust-lang/crates.io-index"
813 -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
814 -
815 -[[package]]
816 -name = "semver"
817 -version = "1.0.27"
818 -source = "registry+https://github.com/rust-lang/crates.io-index"
819 -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
820 -
821 -[[package]]
822 -name = "serde"
823 -version = "1.0.228"
824 -source = "registry+https://github.com/rust-lang/crates.io-index"
825 -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
826 -dependencies = [
827 - "serde_core",
828 - "serde_derive",
829 -]
830 -
831 -[[package]]
832 -name = "serde-wasm-bindgen"
833 -version = "0.6.5"
834 -source = "registry+https://github.com/rust-lang/crates.io-index"
835 -checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
836 -dependencies = [
837 - "js-sys",
838 - "serde",
839 - "wasm-bindgen",
840 -]
841 -
842 -[[package]]
843 -name = "serde_core"
844 -version = "1.0.228"
845 -source = "registry+https://github.com/rust-lang/crates.io-index"
846 -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
847 -dependencies = [
848 - "serde_derive",
849 -]
850 -
851 -[[package]]
852 -name = "serde_derive"
853 -version = "1.0.228"
854 -source = "registry+https://github.com/rust-lang/crates.io-index"
855 -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
856 -dependencies = [
857 - "proc-macro2",
858 - "quote",
859 - "syn",
860 -]
861 -
862 -[[package]]
863 -name = "serde_json"
864 -version = "1.0.145"
865 -source = "registry+https://github.com/rust-lang/crates.io-index"
866 -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
867 -dependencies = [
868 - "itoa",
869 - "memchr",
870 - "ryu",
871 - "serde",
872 - "serde_core",
873 -]
874 -
875 -[[package]]
876 -name = "sha2"
877 -version = "0.10.9"
878 -source = "registry+https://github.com/rust-lang/crates.io-index"
879 -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
880 -dependencies = [
881 - "cfg-if 1.0.4",
882 - "cpufeatures",
883 - "digest",
884 -]
885 -
886 -[[package]]
887 -name = "shlex"
888 -version = "1.3.0"
889 -source = "registry+https://github.com/rust-lang/crates.io-index"
890 -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
891 -
892 -[[package]]
893 -name = "signature"
894 -version = "2.2.0"
895 -source = "registry+https://github.com/rust-lang/crates.io-index"
896 -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
897 -dependencies = [
898 - "rand_core",
899 -]
900 -
901 -[[package]]
902 -name = "slab"
903 -version = "0.4.11"
904 -source = "registry+https://github.com/rust-lang/crates.io-index"
905 -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
906 -
907 -[[package]]
908 -name = "smallvec"
909 -version = "1.15.1"
910 -source = "registry+https://github.com/rust-lang/crates.io-index"
911 -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
912 -
913 -[[package]]
914 -name = "spki"
915 -version = "0.7.3"
916 -source = "registry+https://github.com/rust-lang/crates.io-index"
917 -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
918 -dependencies = [
919 - "base64ct",
920 - "der",
921 -]
922 -
923 -[[package]]
924 -name = "subtle"
925 -version = "2.6.1"
926 -source = "registry+https://github.com/rust-lang/crates.io-index"
927 -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
928 -
929 -[[package]]
930 -name = "syn"
931 -version = "2.0.108"
932 -source = "registry+https://github.com/rust-lang/crates.io-index"
933 -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
934 -dependencies = [
935 - "proc-macro2",
936 - "quote",
937 - "unicode-ident",
938 -]
939 -
940 -[[package]]
941 -name = "tempfile"
942 -version = "3.23.0"
943 -source = "registry+https://github.com/rust-lang/crates.io-index"
944 -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
945 -dependencies = [
946 - "fastrand",
947 - "getrandom 0.3.4",
948 - "once_cell",
949 - "rustix",
950 - "windows-sys",
951 -]
952 -
953 -[[package]]
954 -name = "thiserror"
955 -version = "2.0.17"
956 -source = "registry+https://github.com/rust-lang/crates.io-index"
957 -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
958 -dependencies = [
959 - "thiserror-impl",
960 -]
961 -
962 -[[package]]
963 -name = "thiserror-impl"
964 -version = "2.0.17"
965 -source = "registry+https://github.com/rust-lang/crates.io-index"
966 -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
967 -dependencies = [
968 - "proc-macro2",
969 - "quote",
970 - "syn",
971 -]
972 -
973 -[[package]]
974 -name = "tokio"
975 -version = "1.48.0"
976 -source = "registry+https://github.com/rust-lang/crates.io-index"
977 -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
978 -dependencies = [
979 - "pin-project-lite",
980 - "tokio-macros",
981 -]
982 -
983 -[[package]]
984 -name = "tokio-macros"
985 -version = "2.6.0"
986 -source = "registry+https://github.com/rust-lang/crates.io-index"
987 -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
988 -dependencies = [
989 - "proc-macro2",
990 - "quote",
991 - "syn",
992 -]
993 -
994 -[[package]]
995 -name = "typenum"
996 -version = "1.19.0"
997 -source = "registry+https://github.com/rust-lang/crates.io-index"
998 -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
999 -
1000 -[[package]]
1001 -name = "unicode-ident"
1002 -version = "1.0.20"
1003 -source = "registry+https://github.com/rust-lang/crates.io-index"
1004 -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
1005 -
1006 -[[package]]
1007 -name = "universal-hash"
1008 -version = "0.5.1"
1009 -source = "registry+https://github.com/rust-lang/crates.io-index"
1010 -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
1011 -dependencies = [
1012 - "crypto-common",
1013 - "subtle",
1014 -]
1015 -
1016 -[[package]]
1017 -name = "version_check"
1018 -version = "0.9.5"
1019 -source = "registry+https://github.com/rust-lang/crates.io-index"
1020 -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
1021 -
1022 -[[package]]
1023 -name = "walkdir"
1024 -version = "2.5.0"
1025 -source = "registry+https://github.com/rust-lang/crates.io-index"
1026 -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
1027 -dependencies = [
1028 - "same-file",
1029 - "winapi-util",
1030 -]
1031 -
1032 -[[package]]
1033 -name = "wasi"
1034 -version = "0.11.1+wasi-snapshot-preview1"
1035 -source = "registry+https://github.com/rust-lang/crates.io-index"
1036 -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
1037 -
1038 -[[package]]
1039 -name = "wasip2"
1040 -version = "1.0.1+wasi-0.2.4"
1041 -source = "registry+https://github.com/rust-lang/crates.io-index"
1042 -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
1043 -dependencies = [
1044 - "wit-bindgen",
1045 -]
1046 -
1047 -[[package]]
1048 -name = "wasm-bindgen"
1049 -version = "0.2.105"
1050 -source = "registry+https://github.com/rust-lang/crates.io-index"
1051 -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
1052 -dependencies = [
1053 - "cfg-if 1.0.4",
1054 - "once_cell",
1055 - "rustversion",
1056 - "wasm-bindgen-macro",
1057 - "wasm-bindgen-shared",
1058 -]
1059 -
1060 -[[package]]
1061 -name = "wasm-bindgen-futures"
1062 -version = "0.4.55"
1063 -source = "registry+https://github.com/rust-lang/crates.io-index"
1064 -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
1065 -dependencies = [
1066 - "cfg-if 1.0.4",
1067 - "js-sys",
1068 - "once_cell",
1069 - "wasm-bindgen",
1070 - "web-sys",
1071 -]
1072 -
1073 -[[package]]
1074 -name = "wasm-bindgen-macro"
1075 -version = "0.2.105"
1076 -source = "registry+https://github.com/rust-lang/crates.io-index"
1077 -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
1078 -dependencies = [
1079 - "quote",
1080 - "wasm-bindgen-macro-support",
1081 -]
1082 -
1083 -[[package]]
1084 -name = "wasm-bindgen-macro-support"
1085 -version = "0.2.105"
1086 -source = "registry+https://github.com/rust-lang/crates.io-index"
1087 -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
1088 -dependencies = [
1089 - "bumpalo",
1090 - "proc-macro2",
1091 - "quote",
1092 - "syn",
1093 - "wasm-bindgen-shared",
1094 -]
1095 -
1096 -[[package]]
1097 -name = "wasm-bindgen-shared"
1098 -version = "0.2.105"
1099 -source = "registry+https://github.com/rust-lang/crates.io-index"
1100 -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
1101 -dependencies = [
1102 - "unicode-ident",
1103 -]
1104 -
1105 -[[package]]
1106 -name = "wasm-bindgen-test"
1107 -version = "0.3.55"
1108 -source = "registry+https://github.com/rust-lang/crates.io-index"
1109 -checksum = "bfc379bfb624eb59050b509c13e77b4eb53150c350db69628141abce842f2373"
1110 -dependencies = [
1111 - "js-sys",
1112 - "minicov",
1113 - "wasm-bindgen",
1114 - "wasm-bindgen-futures",
1115 - "wasm-bindgen-test-macro",
1116 -]
1117 -
1118 -[[package]]
1119 -name = "wasm-bindgen-test-macro"
1120 -version = "0.3.55"
1121 -source = "registry+https://github.com/rust-lang/crates.io-index"
1122 -checksum = "085b2df989e1e6f9620c1311df6c996e83fe16f57792b272ce1e024ac16a90f1"
1123 -dependencies = [
1124 - "proc-macro2",
1125 - "quote",
1126 - "syn",
1127 -]
1128 -
1129 -[[package]]
1130 -name = "web-sys"
1131 -version = "0.3.82"
1132 -source = "registry+https://github.com/rust-lang/crates.io-index"
1133 -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
1134 -dependencies = [
1135 - "js-sys",
1136 - "wasm-bindgen",
1137 -]
1138 -
1139 -[[package]]
1140 -name = "wee_alloc"
1141 -version = "0.4.5"
1142 -source = "registry+https://github.com/rust-lang/crates.io-index"
1143 -checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
1144 -dependencies = [
1145 - "cfg-if 0.1.10",
1146 - "libc",
1147 - "memory_units",
1148 - "winapi",
1149 -]
1150 -
1151 -[[package]]
1152 -name = "winapi"
1153 -version = "0.3.9"
1154 -source = "registry+https://github.com/rust-lang/crates.io-index"
1155 -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1156 -dependencies = [
1157 - "winapi-i686-pc-windows-gnu",
1158 - "winapi-x86_64-pc-windows-gnu",
1159 -]
1160 -
1161 -[[package]]
1162 -name = "winapi-i686-pc-windows-gnu"
1163 -version = "0.4.0"
1164 -source = "registry+https://github.com/rust-lang/crates.io-index"
1165 -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1166 -
1167 -[[package]]
1168 -name = "winapi-util"
1169 -version = "0.1.11"
1170 -source = "registry+https://github.com/rust-lang/crates.io-index"
1171 -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
1172 -dependencies = [
1173 - "windows-sys",
1174 -]
1175 -
1176 -[[package]]
1177 -name = "winapi-x86_64-pc-windows-gnu"
1178 -version = "0.4.0"
1179 -source = "registry+https://github.com/rust-lang/crates.io-index"
1180 -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1181 -
1182 -[[package]]
1183 -name = "windows-link"
1184 -version = "0.2.1"
1185 -source = "registry+https://github.com/rust-lang/crates.io-index"
1186 -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
1187 -
1188 -[[package]]
1189 -name = "windows-sys"
1190 -version = "0.61.2"
1191 -source = "registry+https://github.com/rust-lang/crates.io-index"
1192 -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
1193 -dependencies = [
1194 - "windows-link",
1195 -]
1196 -
1197 -[[package]]
1198 -name = "wit-bindgen"
1199 -version = "0.46.0"
1200 -source = "registry+https://github.com/rust-lang/crates.io-index"
1201 -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
1202 -
1203 -[[package]]
1204 -name = "x25519-dalek"
1205 -version = "2.0.1"
1206 -source = "registry+https://github.com/rust-lang/crates.io-index"
1207 -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
1208 -dependencies = [
1209 - "curve25519-dalek",
1210 - "rand_core",
1211 - "serde",
1212 - "zeroize",
1213 -]
1214 -
1215 -[[package]]
1216 -name = "zeroize"
1217 -version = "1.8.2"
1218 -source = "registry+https://github.com/rust-lang/crates.io-index"
1219 -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
1220 -dependencies = [
1221 - "zeroize_derive",
1222 -]
1223 -
1224 -[[package]]
1225 -name = "zeroize_derive"
1226 -version = "1.4.2"
1227 -source = "registry+https://github.com/rust-lang/crates.io-index"
1228 -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
1229 -dependencies = [
1230 - "proc-macro2",
1231 - "quote",
1232 - "syn",
1233 -]
Cargo.toml deleted
-12
@@ -1,12 +0,0 @@
1 -[workspace]
2 -members = ["portal/wasm"]
3 -resolver = "2"
4 -
5 -[profile.release]
6 -opt-level = "z"
7 -lto = true
8 -codegen-units = 1
9 -panic = "abort"
10 -
11 -[profile.release.package."*"]
12 -opt-level = "z"
Dockerfile
+23 -8
@@ -2,22 +2,37 @@ FROM golang:1 AS builder
2
3 WORKDIR /src
4
5 +# Install make and binaryen (for wasm-opt) to use Makefile
6 +RUN apt-get update && apt-get install -y --no-install-recommends binaryen make && rm -rf /var/lib/apt/lists/*
7 +
8 COPY go.mod go.sum ./
6 -RUN --mount=type=cache,target=/go/pkg/mod \
7 - go mod download
9
10 +# Download dependencies
11 +RUN go mod download
12 +
13 +# Copy the rest of the source code
14 COPY . .
10 -# Install make to use Makefile and build the server
11 -RUN apt-get update && apt-get install -y --no-install-recommends make && rm -rf /var/lib/apt/lists/*
15
13 -RUN --mount=type=cache,target=/go/pkg/mod \
14 - make build-server && install -D bin/relay-server /out/relay-server
16 +# Build WASM and server
17 +RUN make build-wasm
18 +
19 +# Build server
20 +RUN make build-server
21
16 -# Stage 3: Minimal runtime image
22 FROM gcr.io/distroless/static-debian12:nonroot
23
19 -COPY --from=builder /out/relay-server /usr/bin/relay-server
24 +# Copy server binary
25 +COPY --from=builder /src/bin/relay-server /usr/bin/relay-server
26 +
27 +# Copy static files for portal frontend
28 +COPY --from=builder /src/dist /app/dist
29 +
30 +# Set default environment variables
31 +ENV STATIC_DIR=/app/dist
32 +ENV PORTAL_DOMAIN=portal.gosuda.org
33
34 +# Expose ports
35 +# 4017: relay server and portal frontend
36 EXPOSE 4017
37
38 ENTRYPOINT ["/usr/bin/relay-server"]
Dockerfile.frontend deleted
-30
@@ -1,30 +0,0 @@
1 -FROM golang:1 AS builder
2 -
3 -WORKDIR /src
4 -
5 -COPY go.mod go.sum ./
6 -RUN --mount=type=cache,target=/go/pkg/mod \
7 - go mod download
8 -
9 -COPY . .
10 -
11 -RUN GOOS=js GOARCH=wasm go build -trimpath -ldflags="-s -w" -o bin/main_hashstr.wasm ./cmd/webclient
12 -
13 -# Calculate SHA256 hash and rename WASM file
14 -RUN HASH=$(sha256sum bin/main_hashstr.wasm | cut -d' ' -f1) && \
15 - mv bin/main_hashstr.wasm bin/main_${HASH}.wasm && \
16 - echo "{\"wasmFile\": \"main_${HASH}.wasm\", \"hash\": \"${HASH}\"}" > bin/manifest.json
17 -
18 -FROM nginx
19 -
20 -COPY --from=builder /src/cmd/webclient/index.html /usr/share/nginx/html/index.html
21 -COPY --from=builder /src/cmd/webclient/portal.mp4 /usr/share/nginx/html/portal.mp4
22 -COPY --from=builder /src/cmd/webclient/service-worker.js /usr/share/nginx/html/service-worker.js
23 -COPY --from=builder /src/cmd/webclient/wasm_exec.js /usr/share/nginx/html/wasm_exec.js
24 -COPY --from=builder /src/bin/main_*.wasm /usr/share/nginx/html/
25 -COPY --from=builder /src/bin/manifest.json /usr/share/nginx/html/manifest.json
26 -
27 -
28 -EXPOSE 80
29 -
30 -CMD ["nginx", "-g", "daemon off;"]
Makefile
+32 -15
@@ -14,28 +14,45 @@ build-protoc:
14 portal/core/proto/rdsec/rdsec.proto \
15 portal/core/proto/rdverb/rdverb.proto
16
17 -# Build WASM artifacts and copy to both embed dir and SDK dir (no external scripts)
17 +# Build WASM artifacts with wasm-opt optimization and generate manifest
18 build-wasm:
19 - @echo "[wasm] building with wasm-pack..."
20 - cd portal/wasm && wasm-pack build --target web --out-dir pkg
21 - @echo "[wasm] copying WASM artifacts to embed dirs..."
22 - mkdir -p cmd/relay-server/wasm
23 - rm -rf cmd/relay-server/wasm/* sdk/wasm/*
24 - cp -R portal/wasm/pkg/. cmd/relay-server/wasm/
25 - @echo "[wasm] copying service workers and E2EE proxy files..."
26 - cp portal/wasm/sw-proxy.js cmd/relay-server/wasm/
27 - cp portal/wasm/sw.js cmd/relay-server/wasm/
28 - @echo "[wasm] copying SecureWebSocket E2EE files..."
29 - cp portal/wasm/secure-websocket.js cmd/relay-server/wasm/
30 - cp portal/wasm/secure-websocket-sw.js cmd/relay-server/wasm/
19 + @echo "[wasm] building webclient WASM..."
20 + @mkdir -p dist
21 +
22 + GOOS=js GOARCH=wasm go build -trimpath -ldflags "-s -w" -o dist/portal.wasm ./cmd/webclient
23 +
24 + @echo "[wasm] optimizing with wasm-opt..."
25 + @if command -v wasm-opt >/dev/null 2>&1; then \
26 + wasm-opt -Oz --enable-bulk-memory dist/portal.wasm -o dist/portal.wasm.tmp && \
27 + mv dist/portal.wasm.tmp dist/portal.wasm; \
28 + echo "[wasm] optimization complete"; \
29 + else \
30 + echo "[wasm] WARNING: wasm-opt not found, skipping optimization"; \
31 + echo "[wasm] Install binaryen for smaller WASM files: brew install binaryen (macOS) or apt-get install binaryen (Linux)"; \
32 + fi
33 +
34 + @echo "[wasm] calculating SHA256 hash..."
35 + @WASM_HASH=$$(shasum -a 256 dist/portal.wasm | awk '{print $$1}'); \
36 + echo "[wasm] SHA256: $$WASM_HASH"; \
37 + cp dist/portal.wasm dist/$$WASM_HASH.wasm; \
38 + echo "{\"wasmFile\":\"$$WASM_HASH.wasm\",\"hash\":\"$$WASM_HASH\"}" > dist/manifest.json; \
39 + echo "[wasm] manifest created"
40 +
41 + @echo "[wasm] copying additional resources..."
42 + @cp cmd/webclient/wasm_exec.js dist/wasm_exec.js
43 + @cp cmd/webclient/service-worker.js dist/service-worker.js
44 + @cp cmd/webclient/index.html dist/portal.html
45 + @cp cmd/webclient/portal.mp4 dist/portal.mp4
46 +
47 + @echo "[wasm] build complete"
48
32 -# Build Go relay server (embeds WASM from cmd/relay-server/wasm)
49 +# Build Go relay server (embeds WASM from cmd/relay-server/static)
50 build-server:
51 @echo "[server] building Go portal..."
52 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o bin/relay-server ./cmd/relay-server
53
54 clean:
55 rm -rf bin
39 - rm -rf cmd/relay-server/wasm
56 + rm -rf dist
57 rm -rf sdk/wasm
58 rm -rf portal/wasm/pkg
cmd/relay-server/frontend.go new
+434
@@ -0,0 +1,434 @@
1 +package main
2 +
3 +import (
4 + "bytes"
5 + "compress/gzip"
6 + "crypto/sha256"
7 + "encoding/hex"
8 + "io"
9 + "net/http"
10 + "os"
11 + "path/filepath"
12 + "strconv"
13 + "strings"
14 + "sync"
15 +
16 + "github.com/andybalholm/brotli"
17 + "github.com/rs/zerolog/log"
18 +)
19 +
20 +// staticDir is the directory where static files are located
21 +var staticDir = "./dist"
22 +
23 +// portalDomain is the domain for portal frontend (e.g., "portal.gosuda.org")
24 +var portalDomain = "portal.gosuda.org"
25 +
26 +// wasmCache stores pre-compressed WASM files in memory
27 +type wasmCacheEntry struct {
28 + original []byte
29 + brotli []byte
30 + gzip []byte
31 + hash string
32 +}
33 +
34 +var (
35 + wasmCache = make(map[string]*wasmCacheEntry)
36 + wasmCacheMu sync.RWMutex
37 +)
38 +
39 +// initWasmCache pre-compresses and caches all WASM files on startup
40 +func initWasmCache() error {
41 + // Read all files in staticDir
42 + entries, err := os.ReadDir(staticDir)
43 + if err != nil {
44 + return err
45 + }
46 +
47 + for _, entry := range entries {
48 + if entry.IsDir() {
49 + continue
50 + }
51 +
52 + name := entry.Name()
53 + // Look for content-addressed WASM files: <64-char-hex>.wasm
54 + if strings.HasSuffix(name, ".wasm") && len(name) == 69 { // 64 + len(".wasm")
55 + hash := strings.TrimSuffix(name, ".wasm")
56 + if isHexString(hash) && len(hash) == 64 {
57 + fullPath := filepath.Join(staticDir, name)
58 + if err := cacheWasmFile(name, fullPath); err != nil {
59 + log.Warn().Err(err).Str("file", name).Msg("failed to cache WASM file")
60 + } else {
61 + log.Info().Str("file", name).Msg("cached and compressed WASM file")
62 + }
63 + }
64 + }
65 + }
66 +
67 + return nil
68 +}
69 +
70 +// cacheWasmFile reads, compresses, and caches a WASM file
71 +func cacheWasmFile(name, fullPath string) error {
72 + // Read original file
73 + original, err := os.ReadFile(fullPath)
74 + if err != nil {
75 + return err
76 + }
77 +
78 + // Verify SHA256 hash matches filename
79 + hash := sha256.Sum256(original)
80 + expectedHash := strings.TrimSuffix(name, ".wasm")
81 + actualHash := hex.EncodeToString(hash[:])
82 + if expectedHash != actualHash {
83 + log.Warn().
84 + Str("file", name).
85 + Str("expected", expectedHash).
86 + Str("actual", actualHash).
87 + Msg("WASM file hash mismatch")
88 + }
89 +
90 + // Compress with brotli (level 11 for maximum compression)
91 + var brBuf bytes.Buffer
92 + brWriter := brotli.NewWriterLevel(&brBuf, 11)
93 + if _, err := brWriter.Write(original); err != nil {
94 + return err
95 + }
96 + if err := brWriter.Close(); err != nil {
97 + return err
98 + }
99 +
100 + // Compress with gzip as fallback
101 + var gzBuf bytes.Buffer
102 + gzWriter := gzip.NewWriter(&gzBuf)
103 + if _, err := gzWriter.Write(original); err != nil {
104 + return err
105 + }
106 + if err := gzWriter.Close(); err != nil {
107 + return err
108 + }
109 +
110 + entry := &wasmCacheEntry{
111 + original: original,
112 + brotli: brBuf.Bytes(),
113 + gzip: gzBuf.Bytes(),
114 + hash: actualHash,
115 + }
116 +
117 + wasmCacheMu.Lock()
118 + wasmCache[name] = entry
119 + wasmCacheMu.Unlock()
120 +
121 + log.Debug().
122 + Str("file", name).
123 + Int("original", len(original)).
124 + Int("brotli", len(entry.brotli)).
125 + Int("gzip", len(entry.gzip)).
126 + Float64("br_ratio", float64(len(entry.brotli))/float64(len(original))*100).
127 + Float64("gz_ratio", float64(len(entry.gzip))/float64(len(original))*100).
128 + Msg("WASM file cached")
129 +
130 + return nil
131 +}
132 +
133 +// createPortalMux creates a new HTTP mux for portal frontend
134 +func createPortalMux() *http.ServeMux {
135 + // Initialize WASM cache on startup
136 + if err := initWasmCache(); err != nil {
137 + log.Error().Err(err).Msg("failed to initialize WASM cache")
138 + }
139 +
140 + mux := http.NewServeMux()
141 +
142 + // Static file handler for /static/
143 + mux.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
144 + path := strings.TrimPrefix(r.URL.Path, "/static/")
145 + servePortalStaticFile(w, r, path)
146 + })
147 +
148 + // Root handler for portal frontend
149 + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
150 + if r.URL.Path == "/" {
151 + serveStaticFile(w, r, "portal.html", "text/html; charset=utf-8")
152 + return
153 + }
154 +
155 + // Try to serve static files, fallback to portal.html for SPA routing
156 + servePortalStatic(w, r)
157 + })
158 +
159 + return mux
160 +}
161 +
162 +// servePortalStaticFile serves static files for portal frontend with caching
163 +func servePortalStaticFile(w http.ResponseWriter, r *http.Request, path string) {
164 + // Check if this is a content-addressed WASM file
165 + if strings.HasSuffix(path, ".wasm") && len(path) == 69 { // 64 + len(".wasm")
166 + hash := strings.TrimSuffix(path, ".wasm")
167 + if isHexString(hash) && len(hash) == 64 {
168 + serveCompressedWasm(w, r, path)
169 + return
170 + }
171 + }
172 +
173 + // Regular static file serving
174 + w.Header().Set("Cache-Control", "public, max-age=3600")
175 + serveStaticFile(w, r, path, "")
176 +}
177 +
178 +// serveCompressedWasm serves pre-compressed WASM files from memory cache
179 +func serveCompressedWasm(w http.ResponseWriter, r *http.Request, path string) {
180 + wasmCacheMu.RLock()
181 + entry, ok := wasmCache[path]
182 + wasmCacheMu.RUnlock()
183 +
184 + if !ok {
185 + log.Debug().Str("path", path).Msg("WASM file not in cache")
186 + http.NotFound(w, r)
187 + return
188 + }
189 +
190 + // Set immutable cache headers for content-addressed files
191 + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
192 + w.Header().Set("Content-Type", "application/wasm")
193 +
194 + // Check Accept-Encoding header for compression support
195 + acceptEncoding := r.Header.Get("Accept-Encoding")
196 +
197 + // Prefer brotli if supported
198 + if strings.Contains(acceptEncoding, "br") {
199 + w.Header().Set("Content-Encoding", "br")
200 + w.Header().Set("Content-Length", strconv.Itoa(len(entry.brotli)))
201 + w.WriteHeader(http.StatusOK)
202 + w.Write(entry.brotli)
203 + log.Debug().
204 + Str("path", path).
205 + Int("size", len(entry.brotli)).
206 + Str("encoding", "brotli").
207 + Msg("served compressed WASM")
208 + return
209 + }
210 +
211 + // Fall back to gzip if supported
212 + if strings.Contains(acceptEncoding, "gzip") {
213 + w.Header().Set("Content-Encoding", "gzip")
214 + w.Header().Set("Content-Length", strconv.Itoa(len(entry.gzip)))
215 + w.WriteHeader(http.StatusOK)
216 + w.Write(entry.gzip)
217 + log.Debug().
218 + Str("path", path).
219 + Int("size", len(entry.gzip)).
220 + Str("encoding", "gzip").
221 + Msg("served compressed WASM")
222 + return
223 + }
224 +
225 + // No compression support - serve original
226 + w.Header().Set("Content-Length", strconv.Itoa(len(entry.original)))
227 + w.WriteHeader(http.StatusOK)
228 + w.Write(entry.original)
229 + log.Debug().
230 + Str("path", path).
231 + Int("size", len(entry.original)).
232 + Str("encoding", "none").
233 + Msg("served uncompressed WASM")
234 +}
235 +
236 +// serveAdminStatic serves static files for admin UI from embedded FS
237 +func serveAdminStatic(w http.ResponseWriter, r *http.Request, path string) {
238 + // Prevent directory traversal
239 + if strings.Contains(path, "..") {
240 + http.Error(w, "Invalid path", http.StatusBadRequest)
241 + return
242 + }
243 +
244 + // Try to read from embedded FS
245 + fullPath := filepath.Join("static", path)
246 + data, err := assetsFS.ReadFile(fullPath)
247 + if err != nil {
248 + log.Debug().Err(err).Str("path", path).Msg("admin static file not found")
249 + http.NotFound(w, r)
250 + return
251 + }
252 +
253 + // Set content type based on extension
254 + ext := filepath.Ext(path)
255 + contentType := getContentType(ext)
256 + if contentType != "" {
257 + w.Header().Set("Content-Type", contentType)
258 + }
259 +
260 + w.Header().Set("Cache-Control", "public, max-age=3600")
261 + w.WriteHeader(http.StatusOK)
262 + w.Write(data)
263 +
264 + log.Debug().
265 + Str("path", path).
266 + Int("size", len(data)).
267 + Msg("served admin static file")
268 +}
269 +
270 +// servePortalStatic serves static files for portal frontend with appropriate cache headers
271 +// Falls back to portal.html for SPA routing (404 -> portal.html)
272 +func servePortalStatic(w http.ResponseWriter, r *http.Request) {
273 + path := strings.TrimPrefix(r.URL.Path, "/")
274 +
275 + // Prevent directory traversal
276 + if strings.Contains(path, "..") {
277 + http.Error(w, "Invalid path", http.StatusBadRequest)
278 + return
279 + }
280 +
281 + // Special handling for specific files
282 + switch path {
283 + case "manifest.json":
284 + w.Header().Set("Cache-Control", "no-cache, must-revalidate")
285 + w.Header().Set("Content-Type", "application/json")
286 + serveStaticFileWithFallback(w, r, path, "application/json")
287 + return
288 +
289 + case "service-worker.js":
290 + w.Header().Set("Cache-Control", "no-cache, must-revalidate")
291 + w.Header().Set("Content-Type", "application/javascript")
292 + serveStaticFileWithFallback(w, r, path, "application/javascript")
293 + return
294 +
295 + case "wasm_exec.js":
296 + w.Header().Set("Cache-Control", "public, max-age=86400")
297 + w.Header().Set("Content-Type", "application/javascript")
298 + serveStaticFileWithFallback(w, r, path, "application/javascript")
299 + return
300 +
301 + case "portal.mp4":
302 + w.Header().Set("Cache-Control", "public, max-age=604800")
303 + w.Header().Set("Content-Type", "video/mp4")
304 + serveStaticFileWithFallback(w, r, path, "video/mp4")
305 + return
306 + }
307 +
308 + // Default caching for other files
309 + w.Header().Set("Cache-Control", "public, max-age=3600")
310 + serveStaticFileWithFallback(w, r, path, "")
311 +}
312 +
313 +// serveStaticFile reads and serves a file from the static directory
314 +func serveStaticFile(w http.ResponseWriter, r *http.Request, path string, contentType string) {
315 + fullPath := filepath.Join(staticDir, path)
316 +
317 + file, err := os.Open(fullPath)
318 + if err != nil {
319 + log.Debug().Err(err).Str("path", path).Msg("static file not found")
320 + http.NotFound(w, r)
321 + return
322 + }
323 + defer file.Close()
324 +
325 + fileInfo, err := file.Stat()
326 + if err != nil {
327 + log.Error().Err(err).Str("path", path).Msg("failed to stat file")
328 + http.Error(w, "Internal server error", http.StatusInternalServerError)
329 + return
330 + }
331 +
332 + // Set content type
333 + if contentType != "" {
334 + w.Header().Set("Content-Type", contentType)
335 + } else {
336 + ext := filepath.Ext(path)
337 + ct := getContentType(ext)
338 + if ct != "" {
339 + w.Header().Set("Content-Type", ct)
340 + }
341 + }
342 +
343 + w.WriteHeader(http.StatusOK)
344 + io.Copy(w, file)
345 +
346 + log.Debug().
347 + Str("path", path).
348 + Int64("size", fileInfo.Size()).
349 + Msg("served static file")
350 +}
351 +
352 +// serveStaticFileWithFallback reads and serves a file from the static directory
353 +// If the file is not found, it falls back to portal.html for SPA routing
354 +func serveStaticFileWithFallback(w http.ResponseWriter, r *http.Request, path string, contentType string) {
355 + fullPath := filepath.Join(staticDir, path)
356 +
357 + file, err := os.Open(fullPath)
358 + if err != nil {
359 + // File not found - fallback to portal.html for SPA routing
360 + log.Debug().Err(err).Str("path", path).Msg("static file not found, serving portal.html")
361 + w.Header().Set("Content-Type", "text/html; charset=utf-8")
362 + serveStaticFile(w, r, "portal.html", "text/html; charset=utf-8")
363 + return
364 + }
365 + defer file.Close()
366 +
367 + fileInfo, err := file.Stat()
368 + if err != nil {
369 + log.Error().Err(err).Str("path", path).Msg("failed to stat file")
370 + http.Error(w, "Internal server error", http.StatusInternalServerError)
371 + return
372 + }
373 +
374 + // Set content type
375 + if contentType != "" {
376 + w.Header().Set("Content-Type", contentType)
377 + } else {
378 + ext := filepath.Ext(path)
379 + ct := getContentType(ext)
380 + if ct != "" {
381 + w.Header().Set("Content-Type", ct)
382 + }
383 + }
384 +
385 + w.WriteHeader(http.StatusOK)
386 + io.Copy(w, file)
387 +
388 + log.Debug().
389 + Str("path", path).
390 + Int64("size", fileInfo.Size()).
391 + Msg("served static file")
392 +}
393 +
394 +// getContentType returns the MIME type for a file extension
395 +func getContentType(ext string) string {
396 + switch ext {
397 + case ".html":
398 + return "text/html; charset=utf-8"
399 + case ".js":
400 + return "application/javascript"
401 + case ".json":
402 + return "application/json"
403 + case ".wasm":
404 + return "application/wasm"
405 + case ".css":
406 + return "text/css"
407 + case ".mp4":
408 + return "video/mp4"
409 + case ".svg":
410 + return "image/svg+xml"
411 + case ".png":
412 + return "image/png"
413 + case ".ico":
414 + return "image/x-icon"
415 + default:
416 + return ""
417 + }
418 +}
419 +
420 +// isPortalSubdomain checks if the host is a portal subdomain (*.{portalDomain})
421 +func isPortalSubdomain(host string) bool {
422 + // Check if it ends with .{portalDomain} or is exactly {portalDomain}
423 + return strings.HasSuffix(host, "."+portalDomain)
424 +}
425 +
426 +// isHexString checks if a string contains only hexadecimal characters
427 +func isHexString(s string) bool {
428 + for _, c := range s {
429 + if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
430 + return false
431 + }
432 + }
433 + return true
434 +}
cmd/relay-server/main.go
+28 -5
@@ -23,16 +23,31 @@ var rootCmd = &cobra.Command{
23 }
24
25 var (
26 - flagBootstraps []string
27 - flagALPN string
28 - flagPort int
26 + flagBootstraps []string
27 + flagALPN string
28 + flagPort int
29 + flagStaticDir string
30 + flagPortalDomain string
31 )
32
33 func init() {
34 + // Get default values from environment variables
35 + defaultStaticDir := os.Getenv("STATIC_DIR")
36 + if defaultStaticDir == "" {
37 + defaultStaticDir = "./dist"
38 + }
39 +
40 + defaultPortalDomain := os.Getenv("PORTAL_DOMAIN")
41 + if defaultPortalDomain == "" {
42 + defaultPortalDomain = "portal.gosuda.org"
43 + }
44 +
45 flags := rootCmd.PersistentFlags()
46 flags.StringArrayVar(&flagBootstraps, "bootstraps", []string{"ws://localhost:4017/relay"}, "bootstrap addresses")
47 flags.StringVar(&flagALPN, "alpn", "http/1.1", "ALPN identifier for this service")
48 flags.IntVar(&flagPort, "port", 4017, "admin UI and HTTP proxy port")
49 + flags.StringVar(&flagStaticDir, "static-dir", defaultStaticDir, "static files directory for portal frontend (env: STATIC_DIR)")
50 + flags.StringVar(&flagPortalDomain, "portal-domain", defaultPortalDomain, "portal domain for frontend serving (env: PORTAL_DOMAIN)")
51 }
52
53 func main() {
@@ -43,16 +58,24 @@ func main() {
58 }
59
60 func runServer(cmd *cobra.Command, args []string) error {
46 - ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
61 + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
62 defer stop()
63
64 + // Set static directory and portal domain
65 + staticDir = flagStaticDir
66 + portalDomain = flagPortalDomain
67 + log.Info().
68 + Str("static_dir", staticDir).
69 + Str("portal_domain", portalDomain).
70 + Msg("[server] frontend configuration")
71 +
72 cred := sdk.NewCredential()
73
74 serv := portal.NewRelayServer(cred, flagBootstraps)
75 serv.Start()
76 defer serv.Stop()
77
55 - // Admin UI + per-peer HTTP proxy
78 + // Admin UI + Relay + Static Frontend
79 httpSrv := serveHTTP(ctx, fmt.Sprintf(":%d", flagPort), serv, cred.ID(), flagBootstraps, stop)
80
81 <-ctx.Done()
cmd/relay-server/static/index.html
+31 -17
@@ -1,31 +1,40 @@
1 <!doctype html>
2 <html lang="ko">
3 +
4 <head>
4 - <meta charset="utf-8"/>
5 + <meta charset="utf-8" />
6 <meta name="viewport" content="width=device-width, initial-scale=1" />
7 <title>Portal</title>
8 <link rel="icon" type="image/x-icon" href="/favicon.ico" />
9 <link rel="stylesheet" href="/static/style.css" />
9 - </head>
10 +</head>
11 +
12 <body>
13 <div class="wrap">
14 <header>
15 <div class="brand">Portal</div>
16 <div class="rightbar">
17 <div class="searchbar" role="search">
16 - <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
17 - <path d="M15.5 14h-.79l-.28-.27a6.471 6.471 0 0 0 1.57-4.23C15.99 6.01 13.48 3.5 10.49 3.5S5 6.01 5 9s2.51 5.5 5.5 5.5c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l4.25 4.25c.41.41 1.08.41 1.49 0 .41-.41.41-1.08 0-1.49L15.5 14Zm-5 0C8.01 14 6 11.99 6 9.5S8.01 5 10.5 5 15 7.01 15 9.5 12.99 14 10.5 14Z"/>
18 + <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
19 + aria-hidden="true">
20 + <path
21 + d="M15.5 14h-.79l-.28-.27a6.471 6.471 0 0 0 1.57-4.23C15.99 6.01 13.48 3.5 10.49 3.5S5 6.01 5 9s2.51 5.5 5.5 5.5c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l4.25 4.25c.41.41 1.08.41 1.49 0 .41-.41.41-1.08 0-1.49L15.5 14Zm-5 0C8.01 14 6 11.99 6 9.5S8.01 5 10.5 5 15 7.01 15 9.5 12.99 14 10.5 14Z" />
22 </svg>
19 - <input id="search" type="text" placeholder="Search by name" aria-label="Search by name" oninput="filterCards(this.value)">
23 + <input id="search" type="text" placeholder="Search by name" aria-label="Search by name"
24 + oninput="filterCards(this.value)">
25 </div>
21 - <a class="gh-btn" href="https://github.com/gosuda/portal" target="_blank" rel="noopener" aria-label="GitHub repository" title="gosuda/portal">
22 - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
23 - <path d="M12 .5C5.73.5.5 5.73.5 12c0 5.08 3.29 9.37 7.86 10.88.58.1.79-.25.79-.56 0-.27-.01-1.16-.02-2.11-3.2.69-3.88-1.39-3.88-1.39-.53-1.35-1.29-1.71-1.29-1.71-1.05-.72.08-.7.08-.7 1.16.08 1.78 1.19 1.78 1.19 1.03 1.77 2.7 1.26 3.36.96.1-.75.4-1.26.73-1.55-2.56-.29-5.26-1.28-5.26-5.72 0-1.26.45-2.3 1.19-3.11-.12-.29-.52-1.45.11-3.02 0 0 .98-.31 3.2 1.19.93-.26 1.94-.39 2.94-.39s2.01.13 2.95.39c2.22-1.5 3.2-1.19 3.2-1.19.63 1.57.23 2.73.12 3.02.74.81 1.19 1.85 1.19 3.11 0 4.45-2.7 5.43-5.28 5.72.41.36.77 1.07.77 2.16 0 1.56-.01 2.81-.01 3.19 0 .31.21.67.8.56C20.22 21.36 23.5 17.08 23.5 12 23.5 5.73 18.27.5 12 .5Z"/>
26 + <a class="gh-btn" href="https://github.com/gosuda/portal" target="_blank" rel="noopener"
27 + aria-label="GitHub repository" title="gosuda/portal">
28 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"
29 + aria-hidden="true">
30 + <path
31 + d="M12 .5C5.73.5.5 5.73.5 12c0 5.08 3.29 9.37 7.86 10.88.58.1.79-.25.79-.56 0-.27-.01-1.16-.02-2.11-3.2.69-3.88-1.39-3.88-1.39-.53-1.35-1.29-1.71-1.29-1.71-1.05-.72.08-.7.08-.7 1.16.08 1.78 1.19 1.78 1.19 1.03 1.77 2.7 1.26 3.36.96.1-.75.4-1.26.73-1.55-2.56-.29-5.26-1.28-5.26-5.72 0-1.26.45-2.3 1.19-3.11-.12-.29-.52-1.45.11-3.02 0 0 .98-.31 3.2 1.19.93-.26 1.94-.39 2.94-.39s2.01.13 2.95.39c2.22-1.5 3.2-1.19 3.2-1.19.63 1.57.23 2.73.12 3.02.74.81 1.19 1.85 1.19 3.11 0 4.45-2.7 5.43-5.28 5.72.41.36.77 1.07.77 2.16 0 1.56-.01 2.81-.01 3.19 0 .31.21.67.8.56C20.22 21.36 23.5 17.08 23.5 12 23.5 5.73 18.27.5 12 .5Z" />
32 </svg>
33 </a>
34 <div class="counter" aria-label="Active devices" title="Active devices">
27 - <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
28 - <circle cx="12" cy="12" r="6"/>
35 + <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
36 + aria-hidden="true">
37 + <circle cx="12" cy="12" r="6" />
38 </svg>
39 <span>{{len .Rows}}</span>
40 </div>
@@ -34,21 +43,25 @@
43 <main>
44 <div class="grid">
45 {{range .Rows}}
37 - <a class="card {{if .Connected}}connected{{else}}disconnected{{end}}" id="peer-{{.Peer}}" data-peer="{{.Peer}}" data-name="{{.Name}}" href="{{.Link}}" title="{{if .Name}}{{.Name}}{{else}}(unnamed){{end}} 열기">
46 + <a class="card {{if .Connected}}connected{{else}}disconnected{{end}}" id="peer-{{.Peer}}" data-peer="{{.Peer}}"
47 + data-name="{{.Name}}" href="{{.Link}}" title="{{if .Name}}{{.Name}}{{else}}(unnamed){{end}} 열기">
48 <div class="card-head">
49 <span class="status-dot {{if .Connected}}ok{{else}}{{if .StaleRed}}bad{{end}}{{end}}"></span>
50 <div class="title">{{if .Name}}{{.Name}}{{else}}(unnamed){{end}}</div>
41 - <svg class="chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
42 - <path d="M9 6l6 6-6 6"/>
51 + <svg class="chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
52 + stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
53 + <path d="M9 6l6 6-6 6" />
54 </svg>
55 </div>
45 - <div class="muted">Last seen <time class="lastseen" datetime="{{.LastSeenISO}}">{{.LastSeenISO}}</time><br/>Active ~{{.LastSeen}} ago</div>
56 + <div class="muted">Last seen <time class="lastseen"
57 + datetime="{{.LastSeenISO}}">{{.LastSeenISO}}</time><br />Active ~{{.LastSeen}} ago</div>
58 <div class="muted idtext" style="margin-top:auto">{{.Peer}}</div>
59 </a>
60 {{else}}
61 <article class="card">
62 <div class="title">No clients discovered</div>
51 - <div class="muted">Start a client and ensure bootstrap URLs point at this server's /relay WebSocket endpoint.</div>
63 + <div class="muted">Start a client and ensure bootstrap URLs point at this server's /relay WebSocket endpoint.
64 + </div>
65 </article>
66 {{end}}
67 </div>
@@ -75,8 +88,9 @@
88 el.textContent = new Intl.DateTimeFormat(undefined, opts).format(d);
89 el.title = d.toString();
90 });
78 - } catch (_) {}
91 + } catch (_) { }
92 })();
93 </script>
94 </body>
82 -</html>
95 +
96 +</html>
\ No newline at end of file
cmd/relay-server/static/style.css
+373 -66
@@ -1,71 +1,378 @@
1 -* { box-sizing: border-box }
1 +* {
2 + box-sizing: border-box
3 +}
4 +
5 :root {
3 - --bg:#fafbff; --panel:#ffffff; --ink:#0f172a; --muted:#6b7280; --line:#e9eef5;
4 - --primary:#2563eb; --ok:#059669; --bad:#b91c1c; --ok-bg:#ecfdf5; --bad-bg:#fee2e2;
5 -
6 - --radius:14px; --radius-pill:999px;
7 - --shadow:0 1px 2px rgba(15,23,42,0.06);
8 - --shadow-hover:0 6px 20px rgba(15,23,42,0.12);
9 - --transition:.2s ease;
10 - --gap:16px; --pad:16px; --header-pad:20px 24px; --wrap-max:980px;
11 - --fs-base:16px; --fs-muted:13px;
12 - --font:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji";
13 -}
14 -
15 -body { margin:0; background:var(--bg); color:var(--ink); font-family:var(--font); font-size:var(--fs-base); line-height:1.6 }
16 -.wrap { max-width:var(--wrap-max); margin:0 auto; padding: calc(var(--pad) * 2) 20px }
17 -header { display:flex; align-items:center; gap:var(--gap); justify-content:space-between; padding:var(--header-pad); background:var(--panel); border:1px solid var(--line); border-radius:var(--radius) }
18 -.brand { display:flex; align-items:center; height:36px; font-weight:800; font-size:24px; letter-spacing:.2px }
19 -.status { color:var(--ok); font-weight:700 }
20 -.app-left { display:flex; align-items:center; gap:12px }
21 -main { margin-top: 22px }
22 -.section { background:var(--panel); border:1px solid var(--line); border-radius:var(--radius); padding:18px; margin-bottom:14px }
23 -.grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap:var(--gap); margin-top:14px }
24 -
25 -.card { background:var(--panel); border:1px solid var(--line); border-radius:var(--radius); padding:var(--pad); display:flex; flex-direction:column; box-shadow:var(--shadow); transition: box-shadow var(--transition), transform var(--transition); text-decoration:none; color:inherit; cursor:pointer; min-height:180px }
26 -.card:hover { box-shadow:var(--shadow-hover); transform: translateY(-2px) }
27 -.card-head { display:flex; align-items:center; gap:10px; margin-bottom:8px }
28 -.chevron { margin-left:auto; width:16px; height:16px; color:var(--muted); opacity:.9; transition: transform var(--transition), color var(--transition), opacity var(--transition) }
29 -.card:hover .chevron { transform: translateX(2px); color:var(--primary); opacity:1 }
30 -.status-dot { width:10px; height:10px; border-radius:var(--radius-pill); background:var(--muted) }
31 -.status-dot.ok { background: var(--ok) }
32 -.status-dot.bad { background: var(--bad) }
33 -.title { font-weight:800; margin:0; font-size:18px }
34 -.muted { color:var(--muted); font-size:var(--fs-muted) }
35 -.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: var(--fs-muted); color:#374151; word-break: break-all }
36 -
37 -.pill { display:inline-flex; align-items:center; gap:8px; padding:6px 10px; border-radius:var(--radius-pill); font-weight:800; font-size:var(--fs-muted) }
38 -.pill.ok { background:var(--ok-bg); color:var(--ok) }
39 -.pill.bad { background:var(--bad-bg); color:var(--bad) }
40 -.pill .dot { width:8px; height:8px; border-radius:var(--radius-pill); background:var(--ok); display:inline-block }
41 -.pill.bad .dot { background:var(--bad) }
42 -.head { display:flex; align-items:center; justify-content:space-between; gap:12px }
43 -.btn { display:inline-block; background:var(--primary); color:#fff; text-decoration:none; border-radius:8px; padding:6px 10px; font-weight:700; font-size:var(--fs-muted); line-height:1; margin-top:6px; transition: filter var(--transition) }
44 -.btn:hover { filter: brightness(0.95) }
45 -.btn.outline { background:transparent; color:var(--primary); border:1px solid var(--primary) }
46 -.actions { margin-top:auto; display:flex; justify-content:flex-end }
47 -.card:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px }
48 -
49 -.searchbar { flex:1 1 320px; width:320px; min-width:80px; max-width:50vw; margin:0; display:flex; align-items:center; gap:10px; height:36px; padding:0 12px; border:1px solid var(--line); border-radius:var(--radius-pill); background:#fff }
50 -.searchbar:focus-within { box-shadow: 0 1px 6px rgba(32,33,36,.28); border-color:#dfe1e5 }
51 -.searchbar input { width:100%; min-width:0; height:100%; border:none; outline:none; font-size:14px; background:transparent; color:#111 }
52 -.searchbar .icon { width:18px; height:18px; color:#9aa0a6 }
53 -.topbar { display:flex; align-items:center; gap:var(--gap); justify-content:space-between }
54 -.rightbar { display:flex; align-items:center; gap:12px; margin-left:auto; min-width:0 }
55 -.gh-btn { width:36px; height:36px; border-radius:var(--radius-pill); display:inline-flex; align-items:center; justify-content:center; color:#000; background:#fff; border:1px solid var(--line); transition: box-shadow var(--transition), transform var(--transition) }
56 -.gh-btn:hover { box-shadow:var(--shadow) }
57 -.counter { display:inline-flex; align-items:center; gap:6px; height:36px; padding:0 10px; border-radius:var(--radius-pill); background:var(--ok-bg); color:var(--ok); font-weight:800 }
58 -.counter .icon { width:18px; height:18px }
6 + --bg: #fafbff;
7 + --panel: #ffffff;
8 + --ink: #0f172a;
9 + --muted: #6b7280;
10 + --line: #e9eef5;
11 + --primary: #2563eb;
12 + --ok: #059669;
13 + --bad: #b91c1c;
14 + --ok-bg: #ecfdf5;
15 + --bad-bg: #fee2e2;
16 +
17 + --radius: 14px;
18 + --radius-pill: 999px;
19 + --shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
20 + --shadow-hover: 0 6px 20px rgba(15, 23, 42, 0.12);
21 + --transition: .2s ease;
22 + --gap: 16px;
23 + --pad: 16px;
24 + --header-pad: 20px 24px;
25 + --wrap-max: 980px;
26 + --fs-base: 16px;
27 + --fs-muted: 13px;
28 + --font: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
29 +}
30 +
31 +body {
32 + margin: 0;
33 + background: var(--bg);
34 + color: var(--ink);
35 + font-family: var(--font);
36 + font-size: var(--fs-base);
37 + line-height: 1.6
38 +}
39 +
40 +.wrap {
41 + max-width: var(--wrap-max);
42 + margin: 0 auto;
43 + padding: calc(var(--pad) * 2) 20px
44 +}
45 +
46 +header {
47 + display: flex;
48 + align-items: center;
49 + gap: var(--gap);
50 + justify-content: space-between;
51 + padding: var(--header-pad);
52 + background: var(--panel);
53 + border: 1px solid var(--line);
54 + border-radius: var(--radius)
55 +}
56 +
57 +.brand {
58 + display: flex;
59 + align-items: center;
60 + height: 36px;
61 + font-weight: 800;
62 + font-size: 24px;
63 + letter-spacing: .2px
64 +}
65 +
66 +.status {
67 + color: var(--ok);
68 + font-weight: 700
69 +}
70 +
71 +.app-left {
72 + display: flex;
73 + align-items: center;
74 + gap: 12px
75 +}
76 +
77 +main {
78 + margin-top: 22px
79 +}
80 +
81 +.section {
82 + background: var(--panel);
83 + border: 1px solid var(--line);
84 + border-radius: var(--radius);
85 + padding: 18px;
86 + margin-bottom: 14px
87 +}
88 +
89 +.grid {
90 + display: grid;
91 + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
92 + gap: var(--gap);
93 + margin-top: 14px
94 +}
95 +
96 +.card {
97 + background: var(--panel);
98 + border: 1px solid var(--line);
99 + border-radius: var(--radius);
100 + padding: var(--pad);
101 + display: flex;
102 + flex-direction: column;
103 + box-shadow: var(--shadow);
104 + transition: box-shadow var(--transition), transform var(--transition);
105 + text-decoration: none;
106 + color: inherit;
107 + cursor: pointer;
108 + min-height: 180px
109 +}
110 +
111 +.card:hover {
112 + box-shadow: var(--shadow-hover);
113 + transform: translateY(-2px)
114 +}
115 +
116 +.card-head {
117 + display: flex;
118 + align-items: center;
119 + gap: 10px;
120 + margin-bottom: 8px
121 +}
122 +
123 +.chevron {
124 + margin-left: auto;
125 + width: 16px;
126 + height: 16px;
127 + color: var(--muted);
128 + opacity: .9;
129 + transition: transform var(--transition), color var(--transition), opacity var(--transition)
130 +}
131 +
132 +.card:hover .chevron {
133 + transform: translateX(2px);
134 + color: var(--primary);
135 + opacity: 1
136 +}
137 +
138 +.status-dot {
139 + width: 10px;
140 + height: 10px;
141 + border-radius: var(--radius-pill);
142 + background: var(--muted)
143 +}
144 +
145 +.status-dot.ok {
146 + background: var(--ok)
147 +}
148 +
149 +.status-dot.bad {
150 + background: var(--bad)
151 +}
152 +
153 +.title {
154 + font-weight: 800;
155 + margin: 0;
156 + font-size: 18px
157 +}
158 +
159 +.muted {
160 + color: var(--muted);
161 + font-size: var(--fs-muted)
162 +}
163 +
164 +.mono {
165 + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
166 + font-size: var(--fs-muted);
167 + color: #374151;
168 + word-break: break-all
169 +}
170 +
171 +.pill {
172 + display: inline-flex;
173 + align-items: center;
174 + gap: 8px;
175 + padding: 6px 10px;
176 + border-radius: var(--radius-pill);
177 + font-weight: 800;
178 + font-size: var(--fs-muted)
179 +}
180 +
181 +.pill.ok {
182 + background: var(--ok-bg);
183 + color: var(--ok)
184 +}
185 +
186 +.pill.bad {
187 + background: var(--bad-bg);
188 + color: var(--bad)
189 +}
190 +
191 +.pill .dot {
192 + width: 8px;
193 + height: 8px;
194 + border-radius: var(--radius-pill);
195 + background: var(--ok);
196 + display: inline-block
197 +}
198 +
199 +.pill.bad .dot {
200 + background: var(--bad)
201 +}
202 +
203 +.head {
204 + display: flex;
205 + align-items: center;
206 + justify-content: space-between;
207 + gap: 12px
208 +}
209 +
210 +.btn {
211 + display: inline-block;
212 + background: var(--primary);
213 + color: #fff;
214 + text-decoration: none;
215 + border-radius: 8px;
216 + padding: 6px 10px;
217 + font-weight: 700;
218 + font-size: var(--fs-muted);
219 + line-height: 1;
220 + margin-top: 6px;
221 + transition: filter var(--transition)
222 +}
223 +
224 +.btn:hover {
225 + filter: brightness(0.95)
226 +}
227 +
228 +.btn.outline {
229 + background: transparent;
230 + color: var(--primary);
231 + border: 1px solid var(--primary)
232 +}
233 +
234 +.actions {
235 + margin-top: auto;
236 + display: flex;
237 + justify-content: flex-end
238 +}
239 +
240 +.card:focus-visible {
241 + outline: 2px solid var(--primary);
242 + outline-offset: 2px
243 +}
244 +
245 +.searchbar {
246 + flex: 1 1 320px;
247 + width: 320px;
248 + min-width: 80px;
249 + max-width: 50vw;
250 + margin: 0;
251 + display: flex;
252 + align-items: center;
253 + gap: 10px;
254 + height: 36px;
255 + padding: 0 12px;
256 + border: 1px solid var(--line);
257 + border-radius: var(--radius-pill);
258 + background: #fff
259 +}
260 +
261 +.searchbar:focus-within {
262 + box-shadow: 0 1px 6px rgba(32, 33, 36, .28);
263 + border-color: #dfe1e5
264 +}
265 +
266 +.searchbar input {
267 + width: 100%;
268 + min-width: 0;
269 + height: 100%;
270 + border: none;
271 + outline: none;
272 + font-size: 14px;
273 + background: transparent;
274 + color: #111
275 +}
276 +
277 +.searchbar .icon {
278 + width: 18px;
279 + height: 18px;
280 + color: #9aa0a6
281 +}
282 +
283 +.topbar {
284 + display: flex;
285 + align-items: center;
286 + gap: var(--gap);
287 + justify-content: space-between
288 +}
289 +
290 +.rightbar {
291 + display: flex;
292 + align-items: center;
293 + gap: 12px;
294 + margin-left: auto;
295 + min-width: 0
296 +}
297 +
298 +.gh-btn {
299 + width: 36px;
300 + height: 36px;
301 + border-radius: var(--radius-pill);
302 + display: inline-flex;
303 + align-items: center;
304 + justify-content: center;
305 + color: #000;
306 + background: #fff;
307 + border: 1px solid var(--line);
308 + transition: box-shadow var(--transition), transform var(--transition)
309 +}
310 +
311 +.gh-btn:hover {
312 + box-shadow: var(--shadow)
313 +}
314 +
315 +.counter {
316 + display: inline-flex;
317 + align-items: center;
318 + gap: 6px;
319 + height: 36px;
320 + padding: 0 10px;
321 + border-radius: var(--radius-pill);
322 + background: var(--ok-bg);
323 + color: var(--ok);
324 + font-weight: 800
325 +}
326 +
327 +.counter .icon {
328 + width: 18px;
329 + height: 18px
330 +}
331
332 /* Wrap long IDs at card bottom */
61 -.idtext { word-break: break-all; overflow-wrap: anywhere }
333 +.idtext {
334 + word-break: break-all;
335 + overflow-wrap: anywhere
336 +}
337
338 @media (max-width: 640px) {
64 - .wrap { padding: 20px 14px }
65 - header { flex-wrap: nowrap; gap: 8px; padding: 16px }
66 - .brand { white-space: nowrap }
67 - .rightbar { margin-left: auto; width: auto; flex-wrap: nowrap; gap: 8px; min-width: 0 }
68 - .searchbar { flex: 1 1 auto; width: auto; min-width: 60px; max-width: none }
69 - .gh-btn { width: 32px; height: 32px }
70 - .counter { height: 32px; padding: 0 8px; font-size: 12px }
71 -}
339 + .wrap {
340 + padding: 20px 14px
341 + }
342 +
343 + header {
344 + flex-wrap: nowrap;
345 + gap: 8px;
346 + padding: 16px
347 + }
348 +
349 + .brand {
350 + white-space: nowrap
351 + }
352 +
353 + .rightbar {
354 + margin-left: auto;
355 + width: auto;
356 + flex-wrap: nowrap;
357 + gap: 8px;
358 + min-width: 0
359 + }
360 +
361 + .searchbar {
362 + flex: 1 1 auto;
363 + width: auto;
364 + min-width: 60px;
365 + max-width: none
366 + }
367 +
368 + .gh-btn {
369 + width: 32px;
370 + height: 32px
371 + }
372 +
373 + .counter {
374 + height: 32px;
375 + padding: 0 8px;
376 + font-size: 12px
377 + }
378 +}
\ No newline at end of file
cmd/relay-server/view.go
+45 -160
@@ -3,17 +3,10 @@ package main
3 import (
4 "context"
5 "embed"
6 - "encoding/json"
6 "fmt"
7 "html/template"
9 - "io/fs"
10 - "net"
8 "net/http"
12 - "net/http/httputil"
13 - "net/url"
14 - "slices"
9 "strings"
16 - "sync"
10 "time"
11
12 "github.com/gorilla/websocket"
@@ -21,163 +14,50 @@ import (
14
15 "gosuda.org/portal/portal"
16 "gosuda.org/portal/portal/utils/wsstream"
24 - "gosuda.org/portal/sdk"
17 )
18
19 //go:embed static
20 var assetsFS embed.FS
21
22 +func serveAsset(mux *http.ServeMux, route, assetPath, contentType string) {
23 + mux.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
24 + b, err := assetsFS.ReadFile(assetPath)
25 + if err != nil {
26 + http.NotFound(w, r)
27 + return
28 + }
29 + if contentType != "" {
30 + w.Header().Set("Content-Type", contentType)
31 + }
32 + w.WriteHeader(http.StatusOK)
33 + _, _ = w.Write(b)
34 + })
35 +}
36 +
37 // serveHTTP builds the HTTP mux and returns the server.
38 func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID string, bootstraps []string, cancel context.CancelFunc) *http.Server {
39 if addr == "" {
40 addr = ":0"
41 }
42
36 - mux := http.NewServeMux()
37 -
43 // Parse template once per server instance (no global var)
44 tmpl := template.Must(template.ParseFS(assetsFS, "static/index.html"))
45
41 - // Serve static assets under /static/ (CSS, images)
42 - if sub, err := fs.Sub(assetsFS, "static"); err == nil {
43 - mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
44 - }
45 -
46 - // Serve embedded favicons (ico/png/svg)
47 - serveAsset := func(route, assetPath, contentType string) {
48 - mux.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
49 - b, err := assetsFS.ReadFile(assetPath)
50 - if err != nil {
51 - http.NotFound(w, r)
52 - return
53 - }
54 - if contentType != "" {
55 - w.Header().Set("Content-Type", contentType)
56 - }
57 - w.WriteHeader(http.StatusOK)
58 - _, _ = w.Write(b)
59 - })
60 - }
61 - serveAsset("/favicon.ico", "static/favicon/favicon.ico", "image/x-icon")
62 - serveAsset("/favicon.png", "static/favicon/favicon.png", "image/png")
63 - serveAsset("/favicon.svg", "static/favicon/favicon.svg", "image/svg+xml")
64 -
65 - // Per-peer HTTP reverse proxy over Portal
66 - // Route: /peer/{leaseID}/*
67 - var (
68 - proxyClient *sdk.RDClient
69 - proxyClientOnce sync.Once
70 - proxyClientErr error
71 - )
72 - // Lazily initialize a client that connects to provided bootstraps or current server
73 - initProxyClient := func(r *http.Request) (*sdk.RDClient, error) {
74 - proxyClientOnce.Do(func() {
75 - bs := bootstraps
76 - if len(bs) == 0 {
77 - // Derive bootstrap from current request host
78 - // Assume same host/port as admin with path /relay
79 - scheme := "ws"
80 - // No TLS handling here; extend to wss if needed in future
81 - bs = []string{fmt.Sprintf("%s://%s/relay", scheme, r.Host)}
82 - }
83 - proxyClient, proxyClientErr = sdk.NewClient(sdk.WithBootstrapServers(bs))
84 - })
85 - return proxyClient, proxyClientErr
86 - }
87 -
88 - mux.HandleFunc("/peer/", func(w http.ResponseWriter, r *http.Request) {
89 - // Expect path /peer/{nameOrID}[/{rest}]
90 - path := strings.TrimPrefix(r.URL.Path, "/peer/")
91 - if path == "" {
92 - http.NotFound(w, r)
93 - return
94 - }
95 - parts := strings.SplitN(path, "/", 2)
96 - nameOrID := parts[0]
97 - rest := "/"
98 - if len(parts) == 2 && parts[1] != "" {
99 - rest = "/" + parts[1]
100 - }
101 -
102 - // Redirect /peer/my-title to /peer/my-title/ for proper relative path resolution
103 - // This ensures that relative paths like "style.css" resolve to "/peer/my-title/style.css"
104 - // instead of "/peer/style.css"
105 - if len(parts) == 1 && !strings.HasSuffix(r.URL.Path, "/") {
106 - redirectURL := r.URL.Path + "/"
107 - if r.URL.RawQuery != "" {
108 - redirectURL += "?" + r.URL.RawQuery
109 - }
110 - http.Redirect(w, r, redirectURL, http.StatusMovedPermanently)
111 - return
112 - }
113 -
114 - // URL decode the name (handles Unicode like 한글 → %ED%95%9C%EA%B8%80)
115 - decodedName, err := url.QueryUnescape(nameOrID)
116 - if err != nil {
117 - log.Warn().Err(err).Str("name", nameOrID).Msg("[server] Failed to decode peer name")
118 - decodedName = nameOrID // Fallback to original if decode fails
119 - }
120 -
121 - // Try to find lease by name first, then by ID
122 - leaseID := ""
123 - leaseEntries := serv.GetAllLeaseEntries()
124 - for _, entry := range leaseEntries {
125 - if entry.Lease.Name == decodedName {
126 - leaseID = string(entry.Lease.Identity.Id)
127 - break
128 - }
129 - }
130 - // If not found by name, assume it's an ID
131 - if leaseID == "" {
132 - leaseID = decodedName
133 - }
134 -
135 - // Get ALPN from lease metadata
136 - alpns := serv.GetLeaseALPNs(leaseID)
137 - if len(alpns) == 0 {
138 - http.Error(w, "lease not found or no ALPN registered", http.StatusNotFound)
139 - return
140 - }
141 -
142 - if !slices.Contains(alpns, "http/1.1") {
143 - http.Error(w, "no http/1.1 ALPN registered", http.StatusNotFound)
144 - return
145 - }
46 + // Create admin UI mux
47 + adminMux := http.NewServeMux()
48
147 - // Temporary credential for this proxy connection
148 - cred := sdk.NewCredential()
49 + // Serve embedded favicons (ico/png/svg) for admin UI
50 + serveAsset(adminMux, "/favicon.ico", "static/favicon/favicon.ico", "image/x-icon")
51 + serveAsset(adminMux, "/favicon.png", "static/favicon/favicon.png", "image/png")
52 + serveAsset(adminMux, "/favicon.svg", "static/favicon/favicon.svg", "image/svg+xml")
53
150 - client, err := initProxyClient(r)
151 - if err != nil {
152 - http.Error(w, "proxy client init: "+err.Error(), http.StatusInternalServerError)
153 - return
154 - }
155 -
156 - // Create a reverse proxy whose transport dials via SDK client
157 - target, _ := url.Parse("http://relay-peer")
158 - proxy := httputil.NewSingleHostReverseProxy(target)
159 - proxy.Transport = &http.Transport{
160 - DialContext: func(c context.Context, network, address string) (net.Conn, error) {
161 - conn, err := client.Dial(cred, leaseID, "http/1.1")
162 - if err != nil {
163 - return nil, err
164 - }
165 - return conn, nil
166 - },
167 - }
168 -
169 - proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, e error) {
170 - log.Error().Err(e).Str("lease", leaseID).Msg("[server] proxy error")
171 - http.Error(rw, "upstream error", http.StatusBadGateway)
172 - }
173 -
174 - // Use default Director; adjust path on a shallow clone
175 - r2 := r.Clone(r.Context())
176 - r2.URL.Path = rest
177 - proxy.ServeHTTP(w, r2)
54 + // Static assets for admin UI
55 + adminMux.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
56 + path := strings.TrimPrefix(r.URL.Path, "/static/")
57 + serveAdminStatic(w, r, path)
58 })
59
180 - mux.HandleFunc("/relay", func(w http.ResponseWriter, r *http.Request) {
60 + adminMux.HandleFunc("/relay", func(w http.ResponseWriter, r *http.Request) {
61 if r.Method != http.MethodGet {
62 w.Header().Set("Allow", http.MethodGet)
63 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@@ -198,8 +78,8 @@ func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID
78 }
79 })
80
201 - // Index page
202 - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
81 + // Admin UI index page
82 + adminMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
83 if r.URL.Path != "/" {
84 http.NotFound(w, r)
85 return
@@ -215,24 +95,31 @@ func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID
95 }
96
97 w.Header().Set("Content-Type", "text/html; charset=utf-8")
218 - log.Debug().Msg("render admin index")
98 if err := tmpl.Execute(w, data); err != nil {
220 - log.Error().Err(err).Msg("[server] render admin index")
99 + log.Error().Err(err).Msg("[server] error rendering admin index")
100 }
101 })
102
224 - mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
225 - type info struct {
226 - Status string `json:"status"`
103 + adminMux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
104 + w.WriteHeader(http.StatusOK)
105 + w.Write([]byte("{\"status\":\"ok\"}"))
106 + })
107 +
108 + // Create portal frontend mux
109 + portalMux := createPortalMux()
110 +
111 + // Top-level handler that routes based on host
112 + topHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
113 + if isPortalSubdomain(r.Host) {
114 + portalMux.ServeHTTP(w, r)
115 + } else {
116 + adminMux.ServeHTTP(w, r)
117 }
228 - resp := info{Status: "ok"}
229 - w.Header().Set("Content-Type", "application/json")
230 - _ = json.NewEncoder(w).Encode(resp)
118 })
119
120 srv := &http.Server{
121 Addr: addr,
235 - Handler: mux,
122 + Handler: topHandler,
123 }
124
125 go func() {
@@ -348,7 +235,7 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
235 dnsLabel = dnsLabel[:8] + "..."
236 }
237
351 - link := fmt.Sprintf("https://%s.portal.gosuda.org/", lease.Name)
238 + link := fmt.Sprintf("https://%s.%s/", lease.Name, portalDomain)
239
240 row := leaseRow{
241 Peer: identityID,
@@ -370,8 +257,6 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
257 }
258
259 var wsUpgrader = websocket.Upgrader{
373 - ReadBufferSize: 1024,
374 - WriteBufferSize: 1024,
260 CheckOrigin: func(r *http.Request) bool {
261 return true
262 },
cmd/webclient/service-worker.js
+7 -5
@@ -12,7 +12,7 @@ let currentCacheVersion = null;
12 // Fetch manifest to get current WASM filename
13 async function fetchManifest() {
14 if (wasmManifest) return wasmManifest;
15 -
15 +
16 try {
17 const response = await fetch(manifest_URL);
18 if (!response.ok) {
@@ -67,8 +67,9 @@ async function runWASM() {
67
68 try {
69 const manifest = await fetchManifest();
70 - const wasm_URL = `/${manifest.wasmFile}`;
71 -
70 + // Use content-addressed path: /static/<sha256>.wasm
71 + const wasm_URL = `/static/${manifest.wasmFile}`;
72 +
73 const go = new Go();
74
75 const cache = await caches.open(currentCacheVersion);
@@ -101,9 +102,10 @@ self.addEventListener('install', (e) => {
102 async function LoadCache() {
103 try {
104 const manifest = await fetchManifest();
104 - const wasm_URL = `/${manifest.wasmFile}`;
105 + // Use content-addressed path: /static/<sha256>.wasm
106 + const wasm_URL = `/static/${manifest.wasmFile}`;
107 const cache = await caches.open(currentCacheVersion);
106 -
108 +
109 await cache.addAll([
110 wasm_URL,
111 wasm_exec_URL,
go.mod
+1
@@ -3,6 +3,7 @@ module gosuda.org/portal
3 go 1.25.3
4
5 require (
6 + github.com/andybalholm/brotli v1.2.0
7 github.com/gorilla/websocket v1.5.3
8 github.com/hashicorp/yamux v0.1.2
9 github.com/planetscale/vtprotobuf v0.6.0
go.sum
+4
@@ -1,3 +1,5 @@
1 +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
2 +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
3 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
4 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
5 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -30,6 +32,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
32 github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
33 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
34 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
35 +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
36 +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
37 golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
38 golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
39 golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=