diff --git a/bun.lock b/bun.lock index a725e42..d99381b 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,7 @@ "@types/bun": "^1.2.21", "@types/luxon": "^3.7.1", "typescript": "^5.9.2", + "vite": "^8.1.5", }, }, }, @@ -23,6 +24,12 @@ "protobufjs", ], "packages": { + "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.4", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="], @@ -67,6 +74,10 @@ "@jsonjoy.com/util": ["@jsonjoy.com/util@1.9.0", "", { "dependencies": { "@jsonjoy.com/buffers": "^1.0.0", "@jsonjoy.com/codegen": "^1.0.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + + "@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], @@ -85,6 +96,38 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + "@swc/core": ["@swc/core@1.15.43", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.27" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.43", "@swc/core-darwin-x64": "1.15.43", "@swc/core-linux-arm-gnueabihf": "1.15.43", "@swc/core-linux-arm64-gnu": "1.15.43", "@swc/core-linux-arm64-musl": "1.15.43", "@swc/core-linux-ppc64-gnu": "1.15.43", "@swc/core-linux-s390x-gnu": "1.15.43", "@swc/core-linux-x64-gnu": "1.15.43", "@swc/core-linux-x64-musl": "1.15.43", "@swc/core-win32-arm64-msvc": "1.15.43", "@swc/core-win32-ia32-msvc": "1.15.43", "@swc/core-win32-x64-msvc": "1.15.43" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw=="], "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.43", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA=="], @@ -131,6 +174,8 @@ "@temporalio/workflow": ["@temporalio/workflow@1.20.3", "", { "dependencies": { "@temporalio/common": "1.20.3", "@temporalio/proto": "1.20.3", "nexus-rpc": "^0.0.2" } }, "sha512-6LRfnWp3HcvqvKFwbOhs5iDQkHNIM5Rxtxhsi2E5ViPDvJMBqlQ0sQ4oRo5SEgr5Bri650i7GfEQQJMVl7xoKg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], @@ -211,6 +256,8 @@ "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.392", "", {}, "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -235,8 +282,12 @@ "fast-uri": ["fast-uri@3.1.3", "", {}, "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fs-monkey": ["fs-monkey@1.1.0", "", {}, "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "glob-to-regex.js": ["glob-to-regex.js@1.2.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ=="], @@ -257,6 +308,30 @@ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "loader-runner": ["loader-runner@4.3.2", "", {}, "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w=="], "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], @@ -275,6 +350,8 @@ "ms": ["ms@3.0.0-canary.1", "", {}, "sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g=="], + "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], "nexus-rpc": ["nexus-rpc@0.0.2", "", {}, "sha512-IWjIExdVYlmwXuzHdY/Q3lXCv1gbqoAXPazQhy2w4Xgtgha3H0OOujEESVPQcFUFMWm+pAk2gKnb57g8S41JZg=="], @@ -283,6 +360,10 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="], + "proto3-json-serializer": ["proto3-json-serializer@2.0.2", "", { "dependencies": { "protobufjs": "^7.2.5" } }, "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ=="], "protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="], @@ -291,6 +372,8 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], @@ -319,6 +402,8 @@ "thingies": ["thingies@2.6.0", "", { "peerDependencies": { "tslib": "^2" } }, "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "tree-dump": ["tree-dump@1.1.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -333,6 +418,8 @@ "uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], + "vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="], + "watchpack": ["watchpack@2.5.2", "", { "dependencies": { "graceful-fs": "^4.1.2" } }, "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg=="], "webpack": ["webpack@5.108.4", "", { "dependencies": { "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.22.2", "es-module-lexer": "^2.1.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "graceful-fs": "^4.2.11", "loader-runner": "^4.3.2", "mime-db": "^1.54.0", "minimizer-webpack-plugin": "^5.6.1", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "watchpack": "^2.5.2", "webpack-sources": "^3.5.0" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-yur8LyJoeiWh47dErD+Ok7vlbmDsJ3UbbRPAoxbGJ54WpE2y5yVo5G/inUzujnYgw3tPmBRdn+G7PoxXaYC33w=="], diff --git a/config/sub2rank.yaml b/config/sub2rank.yaml index 3030b45..00d8abe 100644 --- a/config/sub2rank.yaml +++ b/config/sub2rank.yaml @@ -87,6 +87,7 @@ temporal: runtime: secretsRoot: /root/.unidesk/.state/secrets secretSourcePaths: + /root/.unidesk/.env/unified-admin-password.txt: /root/.unidesk/.env/unified-admin-password.txt platform-infra/sub2api.env: /root/.unidesk/.state/secrets/platform-infra/sub2api.env platform-infra/apistate.env: /root/.unidesk/.state/secrets/platform-infra/apistate.env defaultCliTarget: local @@ -96,6 +97,7 @@ runtime: mode: embedded databasePath: .state/sub2rank.sqlite scoreCachePath: .state/account-scores.json + monitorWorkDir: /root/unidesk production: mode: http baseUrl: https://apistate.hwpod.com @@ -112,8 +114,8 @@ runtime: stateDir: .state/native env: APISTATE_WEB_PASSWORD: - sourceRef: platform-infra/apistate.env - sourceKey: APISTATE_WEB_PASSWORD + sourceRef: /root/.unidesk/.env/unified-admin-password.txt + sourceKey: contents APISTATE_API_KEY: sourceRef: platform-infra/apistate.env sourceKey: APISTATE_API_KEY @@ -150,13 +152,10 @@ runtime: logFile: worker.log web: command: - - bun - - --hot - - src/dev-web.ts + - bunx + - vite - --config - - config/sub2rank.yaml - - --runtime - - native + - vite.config.ts pidFile: web.pid logFile: web.log serverTargets: @@ -168,8 +167,10 @@ runtime: webListenHost: 127.0.0.1 webListenPort: 5173 webApiBaseUrl: http://127.0.0.1:8080 + secureCookies: false databasePath: .state/sub2rank.sqlite scoreCachePath: .state/account-scores.json + monitorWorkDir: /root/unidesk adminTokenEnv: APISTATE_API_KEY sub2apiAdminEmailEnv: SUB2API_ADMIN_EMAIL sub2apiAdminPasswordEnv: SUB2API_ADMIN_PASSWORD @@ -184,8 +185,10 @@ runtime: webListenHost: 127.0.0.1 webListenPort: 5173 webApiBaseUrl: http://127.0.0.1:8080 + secureCookies: true databasePath: /var/lib/sub2rank/sub2rank.sqlite scoreCachePath: /var/lib/sub2rank/account-scores.json + monitorWorkDir: /opt/unidesk adminTokenEnv: APISTATE_API_KEY sub2apiAdminEmailEnv: SUB2API_ADMIN_EMAIL sub2apiAdminPasswordEnv: SUB2API_ADMIN_PASSWORD diff --git a/package.json b/package.json index b54229f..a35b152 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "cli": "bun scripts/apistate-cli.ts --config config/sub2rank.yaml", "api": "bun --hot src/api.ts --config config/sub2rank.yaml --runtime native", "worker": "bun src/worker.ts --config config/sub2rank.yaml --runtime native", - "dev:web": "bun --hot src/dev-web.ts --config config/sub2rank.yaml --runtime native", + "dev:web": "APISTATE_CONFIG_PATH=config/sub2rank.yaml APISTATE_RUNTIME_ID=native bunx vite --config vite.config.ts", "server": "bun src/api.ts --config config/sub2rank.yaml --runtime k8s", "check": "bunx tsc --noEmit" }, @@ -21,7 +21,8 @@ "devDependencies": { "@types/bun": "^1.2.21", "@types/luxon": "^3.7.1", - "typescript": "^5.9.2" + "typescript": "^5.9.2", + "vite": "^8.1.5" }, "trustedDependencies": [ "@swc/core", diff --git a/scripts/src/cli.ts b/scripts/src/cli.ts index 9cd96cc..322541e 100644 --- a/scripts/src/cli.ts +++ b/scripts/src/cli.ts @@ -85,6 +85,7 @@ function help(): Record { "records list|delete", "credit test", "api smoke --over-api", + "workflow status --id ", "native start|stop|status|logs --component api|worker|web [--tail N]", ], output: "k8s-style text by default; add --json for machine output", @@ -130,6 +131,16 @@ function isAppCommand(value: AppCommand | Record): value is App } async function embedded(parsed: Parsed, config: ReturnType, target: EmbeddedCliTarget): Promise { + if (parsed.command.join(" ") === "scores refresh" || parsed.command.join(" ") === "workflow status") { + const temporal = await TemporalGateway.connect(config); + try { + if (parsed.command[0] === "scores") return await temporal.submit({ kind: "scores.refresh" }); + if (!parsed.id) throw new Error("workflow status requires --id"); + return await temporal.status(parsed.id); + } finally { + await temporal.close(); + } + } const command = appCommand(parsed, config); if (!isAppCommand(command)) return command; const context = createEmbeddedContext(config, target); @@ -148,7 +159,11 @@ async function remote(parsed: Parsed, config: ReturnType, tar const [group, action] = parsed.command; if (group === "backend" && action === "check") return await client.backendCheck(); if (group === "scores" && action === "get") return await client.scores(); - if (group === "scores" && action === "refresh") return await client.refreshScores(); + if (group === "scores" && action === "refresh") return await client.workflowSubmit({ kind: "scores.refresh" }); + if (group === "workflow" && action === "status") { + if (!parsed.id) throw new Error("workflow status requires --id"); + return await client.workflowStatus(parsed.id); + } if (group === "lottery" && action === "status") return await client.status(); if (group === "lottery" && action === "draw") return parsed.confirm ? await client.draw() : { ok: true, mutation: false, action: "lottery-draw", hint: "add --confirm to execute" }; if (group === "lottery" && action === "reset") { diff --git a/src/account-score-service.ts b/src/account-score-service.ts index 1fb797f..1ad1868 100644 --- a/src/account-score-service.ts +++ b/src/account-score-service.ts @@ -48,7 +48,7 @@ export class AccountScoreService { private inFlight: Promise | null = null; private snapshot: ScoreSnapshot; - constructor(private readonly config: AppConfig, private readonly cachePath: string) { + constructor(private readonly config: AppConfig, private readonly cachePath: string, private readonly cliWorkDir: string) { this.snapshot = this.readCache(); } @@ -125,7 +125,7 @@ export class AccountScoreService { private async invoke(scope: string[]): Promise> { const args = [ - resolve(this.config.monitor.cli.workDir, this.config.monitor.cli.entrypoint), + resolve(this.cliWorkDir, this.config.monitor.cli.entrypoint), "platform-infra", "sub2api", "codex-pool", "runtime", "errors", "--target", this.config.monitor.target, "--since", this.config.monitor.scoreWindow, @@ -133,7 +133,7 @@ export class AccountScoreService { "--raw", ]; const process = Bun.spawn([this.config.monitor.cli.executable, ...args], { - cwd: this.config.monitor.cli.workDir, + cwd: this.cliWorkDir, stdout: "pipe", stderr: "pipe", env: { ...Bun.env, UNIDESK_MAIN_SERVER_IP: this.config.monitor.cli.mainServerHost }, diff --git a/src/admin-http-client.ts b/src/admin-http-client.ts index 6df6cbe..4512410 100644 --- a/src/admin-http-client.ts +++ b/src/admin-http-client.ts @@ -38,4 +38,10 @@ export class AdminHttpClient { } ranking(): Promise> { return this.request("/api/ranking"); } lottery(): Promise> { return this.request("/api/lottery"); } + workflowSubmit(command: Record): Promise> { + return this.request("/api/admin/workflows", { method: "POST", body: JSON.stringify({ command }) }); + } + workflowStatus(id: string): Promise> { + return this.request(`/api/admin/workflows/${encodeURIComponent(id)}`); + } } diff --git a/src/api.ts b/src/api.ts index e60d870..5beeb35 100644 --- a/src/api.ts +++ b/src/api.ts @@ -18,7 +18,7 @@ const dispatcher = new ApplicationDispatcher({ lottery: context.service, scores: const server = Bun.serve({ hostname: target.listenHost, port: target.listenPort, - fetch: createHandler(dispatcher, config, context.auth, adminToken), + fetch: createHandler(dispatcher, config, context.auth, adminToken, target.secureCookies), }); console.log(JSON.stringify({ diff --git a/src/bootstrap.ts b/src/bootstrap.ts index e4d92fc..bca1ca0 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -18,7 +18,7 @@ export interface AppContext { export function createEmbeddedContext(config: AppConfig, target: EmbeddedCliTarget): AppContext { const store = new LotteryStore(config, resolveDataPath(config, target.databasePath)); const client = new Sub2ApiClient(config, readSub2ApiCredentials(config)); - const monitor = new AccountScoreService(config, resolveDataPath(config, target.scoreCachePath)); + const monitor = new AccountScoreService(config, resolveDataPath(config, target.scoreCachePath), target.monitorWorkDir); return { service: new LotteryService(config, store, client), store, @@ -39,7 +39,7 @@ export function createServerContext(config: AppConfig, target: ServerTarget): Ap } const store = new LotteryStore(config, resolveDataPath(config, target.databasePath)); const client = new Sub2ApiClient(config, { email, password }); - const monitor = new AccountScoreService(config, resolveDataPath(config, target.scoreCachePath)); + const monitor = new AccountScoreService(config, resolveDataPath(config, target.scoreCachePath), target.monitorWorkDir); return { service: new LotteryService(config, store, client), store, diff --git a/src/config.ts b/src/config.ts index 4cf5253..61e9a20 100644 --- a/src/config.ts +++ b/src/config.ts @@ -80,6 +80,7 @@ export interface EmbeddedCliTarget { mode: "embedded"; databasePath: string; scoreCachePath: string; + monitorWorkDir: string; } export interface HttpCliTarget { @@ -96,8 +97,10 @@ export interface ServerTarget { webListenHost: string; webListenPort: number; webApiBaseUrl: string; + secureCookies: boolean; databasePath: string; scoreCachePath: string; + monitorWorkDir: string; adminTokenEnv: string; sub2apiAdminEmailEnv: string; sub2apiAdminPasswordEnv: string; @@ -210,6 +213,7 @@ export function loadConfig(path: string): AppConfig { mode, databasePath: stringValue(target, "databasePath", `runtime.cliTargets.${id}`), scoreCachePath: stringValue(target, "scoreCachePath", `runtime.cliTargets.${id}`), + monitorWorkDir: stringValue(target, "monitorWorkDir", `runtime.cliTargets.${id}`), }; else if (mode === "http") cliTargets[id] = { mode, baseUrl: stringValue(target, "baseUrl", `runtime.cliTargets.${id}`), adminToken: secretRef(target.adminToken, `runtime.cliTargets.${id}.adminToken`) }; else throw new Error(`runtime.cliTargets.${id}.mode must be embedded or http`); @@ -225,8 +229,10 @@ export function loadConfig(path: string): AppConfig { webListenHost: stringValue(target, "webListenHost", `runtime.serverTargets.${id}`), webListenPort: numberValue(target, "webListenPort", `runtime.serverTargets.${id}`, 1), webApiBaseUrl: stringValue(target, "webApiBaseUrl", `runtime.serverTargets.${id}`), + secureCookies: booleanValue(target, "secureCookies", `runtime.serverTargets.${id}`), databasePath: stringValue(target, "databasePath", `runtime.serverTargets.${id}`), scoreCachePath: stringValue(target, "scoreCachePath", `runtime.serverTargets.${id}`), + monitorWorkDir: stringValue(target, "monitorWorkDir", `runtime.serverTargets.${id}`), adminTokenEnv: stringValue(target, "adminTokenEnv", `runtime.serverTargets.${id}`), sub2apiAdminEmailEnv: stringValue(target, "sub2apiAdminEmailEnv", `runtime.serverTargets.${id}`), sub2apiAdminPasswordEnv: stringValue(target, "sub2apiAdminPasswordEnv", `runtime.serverTargets.${id}`), diff --git a/src/dev-web.ts b/src/dev-web.ts deleted file mode 100644 index b177231..0000000 --- a/src/dev-web.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { resolve } from "node:path"; -import { loadConfig } from "./config"; -import { requiredOption } from "./runtime-args"; - -const config = loadConfig(requiredOption("--config")); -const runtimeId = requiredOption("--runtime"); -const target = config.runtime.serverTargets[runtimeId]; -if (!target) throw new Error(`runtime.serverTargets.${runtimeId} does not exist`); -const staticRoot = resolve(import.meta.dir, "../static"); - -const contentTypes: Record = { - ".css": "text/css; charset=utf-8", - ".html": "text/html; charset=utf-8", - ".js": "text/javascript; charset=utf-8", -}; - -function staticName(pathname: string): string | null { - if (pathname === "/") return "login.html"; - const value = pathname.slice(1); - if (!["login.html", "scores.html", "ranking.html", "lottery.html", "app.js", "styles.css"].includes(value)) return null; - return value; -} - -const server = Bun.serve({ - hostname: target.webListenHost, - port: target.webListenPort, - async fetch(request) { - const url = new URL(request.url); - if (url.pathname.startsWith("/api/") || url.pathname === "/health") { - const upstream = new URL(url.pathname + url.search, target.webApiBaseUrl); - return await fetch(new Request(upstream, request)); - } - const pageAlias = ({ "/login": "login.html", "/scores": "scores.html", "/ranking": "ranking.html", "/lottery": "lottery.html" } as Record)[url.pathname]; - const name = pageAlias ?? staticName(url.pathname); - if (!name) return Response.json({ ok: false, error: "not found" }, { status: 404 }); - const file = Bun.file(resolve(staticRoot, name)); - if (!(await file.exists())) return Response.json({ ok: false, error: "not found" }, { status: 404 }); - const extension = name.slice(name.lastIndexOf(".")); - return new Response(file, { headers: { "content-type": contentTypes[extension] ?? "application/octet-stream", "cache-control": "no-store" } }); - }, -}); - -console.log(JSON.stringify({ - ok: true, - component: "apistate-web-native", - runtime: runtimeId, - listen: server.url.toString(), - apiBaseUrl: target.webApiBaseUrl, - hotReload: true, - valuesPrinted: false, -})); - -for (const signal of ["SIGINT", "SIGTERM"] as const) process.on(signal, () => { - server.stop(true); - process.exit(0); -}); diff --git a/src/dispatcher.ts b/src/dispatcher.ts index 0b7eeda..6044f64 100644 --- a/src/dispatcher.ts +++ b/src/dispatcher.ts @@ -37,6 +37,17 @@ export class ApplicationDispatcher { if (!this.temporal) throw new Error(`command ${command.kind} requires Temporal`); return await this.temporal.execute(command); } + + async submit(command: AppCommand): Promise { + if (!usesWorkflow(command)) throw new Error(`command ${command.kind} does not use Temporal`); + if (!this.temporal) throw new Error(`command ${command.kind} requires Temporal`); + return await this.temporal.submit(command); + } + + async workflowStatus(workflowId: string): Promise { + if (!this.temporal) throw new Error("workflow status requires Temporal"); + return await this.temporal.status(workflowId); + } } export function createActivities(services: DispatcherServices): { executeOperation(request: OperationRequest): Promise } { diff --git a/src/http.ts b/src/http.ts index babbd54..8af09f1 100644 --- a/src/http.ts +++ b/src/http.ts @@ -47,6 +47,7 @@ export function createHandler( config: AppConfig, auth: WebAuthSecrets, legacyAdminToken: string, + secureCookies: boolean, ): (request: Request) => Promise { return async (request) => { const url = new URL(request.url); @@ -60,10 +61,10 @@ export function createHandler( if (request.method === "POST" && url.pathname === "/api/login") { const input = await body(request); if (!validLogin(input.username, input.password, config, auth)) return json({ ok: false, error: "用户名或密码错误" }, 401); - return json({ ok: true, username: config.webAuth.username }, 200, { "set-cookie": createSessionCookie(config, auth) }); + return json({ ok: true, username: config.webAuth.username }, 200, { "set-cookie": createSessionCookie(config, auth, secureCookies) }); } if (request.method === "POST" && url.pathname === "/api/logout") { - return json({ ok: true }, 200, { "set-cookie": clearSessionCookie(config) }); + return json({ ok: true }, 200, { "set-cookie": clearSessionCookie(config, secureCookies) }); } if ((request.method === "GET" || request.method === "HEAD") && url.pathname === "/login") { return session ? redirect("/scores") : await staticFile("login.html", "text/html; charset=utf-8"); @@ -107,6 +108,17 @@ export function createHandler( return json(await dispatcher.dispatch({ kind: "records.list", limit })); } if (request.method === "DELETE" && url.pathname.startsWith("/api/admin/records/")) return json(await dispatcher.dispatch({ kind: "records.delete", id: decodeURIComponent(url.pathname.slice("/api/admin/records/".length)) })); + if (request.method === "POST" && url.pathname === "/api/admin/workflows") { + const input = await body(request); + const command = input.command as Record | undefined; + if (command?.kind !== "scores.refresh") return json({ ok: false, error: "command.kind must be scores.refresh" }, 400); + return json(await dispatcher.submit({ kind: "scores.refresh" })); + } + if (request.method === "GET" && url.pathname.startsWith("/api/admin/workflows/")) { + const workflowId = decodeURIComponent(url.pathname.slice("/api/admin/workflows/".length)); + if (!workflowId) return json({ ok: false, error: "workflow id is required" }, 400); + return json(await dispatcher.workflowStatus(workflowId)); + } if (request.method === "POST" && url.pathname === "/api/admin/credit-test") { const input = await body(request); if (typeof input.execute !== "boolean") return json({ ok: false, error: "execute must be boolean" }, 400); diff --git a/src/native-services.ts b/src/native-services.ts index d2012f7..c93b912 100644 --- a/src/native-services.ts +++ b/src/native-services.ts @@ -51,6 +51,8 @@ export function nativeStart(config: AppConfig, component: NativeServiceId): Reco rmSync(target.pid, { force: true }); const env: Record = { ...process.env } as Record; for (const [targetKey, ref] of Object.entries(config.runtime.native.env)) env[targetKey] = readSecret(config, ref); + env.APISTATE_CONFIG_PATH = config.configPath; + env.APISTATE_RUNTIME_ID = "native"; const log = openSync(target.log, "a", 0o600); try { const child = spawn(service.command[0]!, service.command.slice(1), { diff --git a/src/secrets.ts b/src/secrets.ts index 8cc7e54..8323c6a 100644 --- a/src/secrets.ts +++ b/src/secrets.ts @@ -19,7 +19,8 @@ function parseEnv(text: string): Map { export function readSecret(config: AppConfig, ref: SecretRef): string { const path = config.runtime.secretSourcePaths[ref.sourceRef]; if (!path) throw new Error(`runtime.secretSourcePaths is missing sourceRef ${ref.sourceRef}`); - const value = parseEnv(readFileSync(path, "utf8")).get(ref.sourceKey); + const text = readFileSync(path, "utf8"); + const value = ref.sourceKey === "contents" ? text.trim() : parseEnv(text).get(ref.sourceKey); if (!value) throw new Error(`secret source ${ref.sourceRef} is missing key ${ref.sourceKey}`); return value; } diff --git a/src/temporal-client.ts b/src/temporal-client.ts index 690861e..8fbfb9c 100644 --- a/src/temporal-client.ts +++ b/src/temporal-client.ts @@ -22,8 +22,13 @@ export class TemporalGateway { } async execute(command: AppCommand): Promise { + const submitted = await this.submit(command); + return await this.client.workflow.getHandle(submitted.workflowId).result(); + } + + async submit(command: AppCommand): Promise<{ ok: true; workflowId: string; runId: string; state: "submitted" }> { const operation: OperationRequest = { operationId: randomUUID(), command }; - return await this.client.workflow.execute("operationWorkflow", { + const handle = await this.client.workflow.start("operationWorkflow", { taskQueue: this.config.temporal.taskQueue, workflowId: `apistate-${command.kind.replaceAll(".", "-")}-${operation.operationId}`, workflowExecutionTimeout: this.config.temporal.workflowExecutionTimeout, @@ -33,6 +38,21 @@ export class TemporalGateway { maximumAttempts: this.config.temporal.retry.maximumAttempts, }], }); + return { ok: true, workflowId: handle.workflowId, runId: handle.firstExecutionRunId, state: "submitted" }; + } + + async status(workflowId: string): Promise> { + const handle = this.client.workflow.getHandle(workflowId); + const description = await handle.describe(); + const state = description.status.name.toLocaleLowerCase("en-US"); + const terminal = ["completed", "failed", "cancelled", "terminated", "timed_out"].includes(state); + let result: unknown = null; + let error: string | null = null; + if (state === "completed") result = await handle.result(); + else if (terminal) { + try { await handle.result(); } catch (caught) { error = caught instanceof Error ? caught.message : String(caught); } + } + return { ok: state === "completed" || !terminal, workflowId, runId: description.runId, state, terminal, result, error }; } async ensureScoreSchedule(): Promise<{ started: boolean; workflowId: string }> { diff --git a/src/web-auth.ts b/src/web-auth.ts index 9593c0e..657130d 100644 --- a/src/web-auth.ts +++ b/src/web-auth.ts @@ -42,17 +42,17 @@ export function sessionAuthorized(request: Request, config: AppConfig, secrets: } } -export function createSessionCookie(config: AppConfig, secrets: WebAuthSecrets): string { +export function createSessionCookie(config: AppConfig, secrets: WebAuthSecrets, secure: boolean): string { const payload = Buffer.from(JSON.stringify({ user: config.webAuth.username, exp: Date.now() + config.webAuth.sessionTtlSeconds * 1000, })).toString("base64url"); const value = `${payload}.${signature(payload, secrets.sessionSecret)}`; - return `${config.webAuth.cookieName}=${value}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=${config.webAuth.sessionTtlSeconds}`; + return `${config.webAuth.cookieName}=${value}; Path=/; HttpOnly;${secure ? " Secure;" : ""} SameSite=Strict; Max-Age=${config.webAuth.sessionTtlSeconds}`; } -export function clearSessionCookie(config: AppConfig): string { - return `${config.webAuth.cookieName}=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0`; +export function clearSessionCookie(config: AppConfig, secure: boolean): string { + return `${config.webAuth.cookieName}=; Path=/; HttpOnly;${secure ? " Secure;" : ""} SameSite=Strict; Max-Age=0`; } export function validLogin(username: unknown, password: unknown, config: AppConfig, secrets: WebAuthSecrets): boolean { diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..e44b0a6 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from "vite"; +import { loadConfig } from "./src/config"; + +const configPath = process.env.APISTATE_CONFIG_PATH; +const runtimeId = process.env.APISTATE_RUNTIME_ID; +if (!configPath) throw new Error("Vite requires APISTATE_CONFIG_PATH"); +if (!runtimeId) throw new Error("Vite requires APISTATE_RUNTIME_ID"); + +const config = loadConfig(configPath); +const target = config.runtime.serverTargets[runtimeId]; +if (!target) throw new Error(`runtime.serverTargets.${runtimeId} does not exist`); + +export default defineConfig({ + root: "static", + server: { + host: target.webListenHost, + port: target.webListenPort, + strictPort: true, + watch: { usePolling: true, interval: 300 }, + proxy: { + "/api": { target: target.webApiBaseUrl, changeOrigin: true }, + "/health": { target: target.webApiBaseUrl, changeOrigin: true }, + }, + }, +});