feat: 建立 ApiState Temporal native 开发链

This commit is contained in:
root
2026-07-17 03:15:50 +02:00
parent ac695021e9
commit d203b22183
19 changed files with 1099 additions and 172 deletions
+2 -3
View File
@@ -1,4 +1,4 @@
FROM oven/bun:1.2.21-slim
FROM oven/bun:1.3.13-slim
WORKDIR /app
@@ -8,7 +8,6 @@ RUN apt-get update \
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
COPY config ./config
COPY src ./src
COPY static ./static
@@ -16,4 +15,4 @@ COPY static ./static
ENV NODE_ENV=production
EXPOSE 8080
CMD ["bun", "src/server.ts", "--config", "config/sub2rank.yaml", "--runtime", "k8s"]
CMD ["bun", "src/api.ts", "--config", "config/sub2rank.yaml", "--runtime", "k8s"]
+337
View File
@@ -5,6 +5,9 @@
"": {
"name": "sub2rank",
"dependencies": {
"@temporalio/client": "^1.20.3",
"@temporalio/worker": "^1.20.3",
"@temporalio/workflow": "^1.20.3",
"luxon": "^3.7.2",
"yaml": "^2.8.1",
},
@@ -15,21 +18,355 @@
},
},
},
"trustedDependencies": [
"@swc/core",
"protobufjs",
],
"packages": {
"@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=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
"@jsonjoy.com/base64": ["@jsonjoy.com/base64@1.1.2", "", { "peerDependencies": { "tslib": "2" } }, "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA=="],
"@jsonjoy.com/buffers": ["@jsonjoy.com/buffers@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw=="],
"@jsonjoy.com/codegen": ["@jsonjoy.com/codegen@1.0.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g=="],
"@jsonjoy.com/fs-core": ["@jsonjoy.com/fs-core@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-zs2TAq7Six5jgMuoMNjpspAvOP3mhtgq/k1UyQodEzCtQi/N83y2/y+zcvnZSGp/Rxq96DBN+bValOBQAyn/ew=="],
"@jsonjoy.com/fs-fsa": ["@jsonjoy.com/fs-fsa@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.64.0", "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-nMWOVbkLFyEgmXZih3wyvxA9XpgyyqyfrINMHvEFqhi7uqfRl7c9ERJt6yX7vgMPrB9Uo+OJO+Spa0cFzPD01w=="],
"@jsonjoy.com/fs-node": ["@jsonjoy.com/fs-node@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.64.0", "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "@jsonjoy.com/fs-print": "4.64.0", "@jsonjoy.com/fs-snapshot": "4.64.0", "glob-to-regex.js": "^1.0.0", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-dO+NNkODbUli4uV42bcNrrLvq5rE7SNpdZ5TNd0dtbLsAaNK3MDiIC9lUi+brboGoIjW6vd2fB1qao60nrk5xA=="],
"@jsonjoy.com/fs-node-builtins": ["@jsonjoy.com/fs-node-builtins@4.64.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-/o7WRFhUWaM/fOrslwLZGnzn4RmRILykn+lAL+mNObqqRNw+CQSiij6hpCeZ+C7buhdoVo7go/OYqzaSUfDYmA=="],
"@jsonjoy.com/fs-node-to-fsa": ["@jsonjoy.com/fs-node-to-fsa@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-fsa": "4.64.0", "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-WDD9WVs0hb7UAEKTgZW2f66WDrbj7gIIWwpP3spbLyXa0rghtUaFTB8L4gdR3ZCWwiKIsj38/CNijpVmpnuPUw=="],
"@jsonjoy.com/fs-node-utils": ["@jsonjoy.com/fs-node-utils@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-node-builtins": "4.64.0", "glob-to-regex.js": "^1.0.1" }, "peerDependencies": { "tslib": "2" } }, "sha512-k5Indsx9hWW9xSF7Y6oSKKwtCUNhzZxadub3owhIlitc+iMRVlPPdX2duTKQWBL3qNWpXya8jykgaaWpheeS4w=="],
"@jsonjoy.com/fs-print": ["@jsonjoy.com/fs-print@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-node-utils": "4.64.0", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-PHZFccchvkhWrwPWHjmVAhbC3vSHCtyZvlZfJJ3ho2bnzl450hXri6/8e6pbkWdH+SkmLXNml0sV8e5HDAfxKw=="],
"@jsonjoy.com/fs-snapshot": ["@jsonjoy.com/fs-snapshot@4.64.0", "", { "dependencies": { "@jsonjoy.com/buffers": "^17.65.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "@jsonjoy.com/json-pack": "^17.65.0", "@jsonjoy.com/util": "^17.65.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-oM7UDeL83q6NBzzsfKAsYKXKVXlykKFqqOLh4xZZKAzzROTlInkPbc6LTDGThEOnPiFiUzA7tYziHG9xavd76Q=="],
"@jsonjoy.com/json-pack": ["@jsonjoy.com/json-pack@1.21.0", "", { "dependencies": { "@jsonjoy.com/base64": "^1.1.2", "@jsonjoy.com/buffers": "^1.2.0", "@jsonjoy.com/codegen": "^1.0.0", "@jsonjoy.com/json-pointer": "^1.0.2", "@jsonjoy.com/util": "^1.9.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg=="],
"@jsonjoy.com/json-pointer": ["@jsonjoy.com/json-pointer@1.0.2", "", { "dependencies": { "@jsonjoy.com/codegen": "^1.0.0", "@jsonjoy.com/util": "^1.9.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg=="],
"@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=="],
"@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=="],
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="],
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="],
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="],
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="],
"@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=="],
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.43", "", { "os": "darwin", "cpu": "x64" }, "sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ=="],
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.43", "", { "os": "linux", "cpu": "arm" }, "sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA=="],
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.43", "", { "os": "linux", "cpu": "arm64" }, "sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw=="],
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.43", "", { "os": "linux", "cpu": "arm64" }, "sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ=="],
"@swc/core-linux-ppc64-gnu": ["@swc/core-linux-ppc64-gnu@1.15.43", "", { "os": "linux", "cpu": "ppc64" }, "sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg=="],
"@swc/core-linux-s390x-gnu": ["@swc/core-linux-s390x-gnu@1.15.43", "", { "os": "linux", "cpu": "s390x" }, "sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA=="],
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.43", "", { "os": "linux", "cpu": "x64" }, "sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg=="],
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.43", "", { "os": "linux", "cpu": "x64" }, "sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ=="],
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.43", "", { "os": "win32", "cpu": "arm64" }, "sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw=="],
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.43", "", { "os": "win32", "cpu": "ia32" }, "sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g=="],
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.43", "", { "os": "win32", "cpu": "x64" }, "sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg=="],
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
"@swc/types": ["@swc/types@0.1.27", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg=="],
"@temporalio/activity": ["@temporalio/activity@1.20.3", "", { "dependencies": { "@temporalio/client": "1.20.3", "@temporalio/common": "1.20.3" } }, "sha512-7jtIkCkUe6wwVrnlCd/7DmnVcu1/r4KFvVHvPVszd/EXAz1ziYfsjhenLfxWIvyxSIU9v3tC96uHODN3ABdv4A=="],
"@temporalio/client": ["@temporalio/client@1.20.3", "", { "dependencies": { "@grpc/grpc-js": "^1.12.4", "@temporalio/common": "1.20.3", "@temporalio/proto": "1.20.3", "abort-controller": "^3.0.0", "long": "^5.2.3", "nexus-rpc": "^0.0.2", "uuid": "^11.1.0" } }, "sha512-RmEX0Z7h3mWq8PMqeDSbbDZPK8IweajGnRsghiJI0fnaWx61YeW8bhAAKfD/iSop+0PM0oe0KaW5XPAKEY4XEw=="],
"@temporalio/common": ["@temporalio/common@1.20.3", "", { "dependencies": { "@temporalio/proto": "1.20.3", "long": "^5.2.3", "ms": "3.0.0-canary.1", "nexus-rpc": "^0.0.2", "proto3-json-serializer": "^2.0.0" } }, "sha512-VM9GyUAa7tl413nM24mcBhUfUeqcGazmDkx/GfDA0c+KuEVusclMg9eqa63EDv/rguC/YhWaXu6q5Kl8+99GSw=="],
"@temporalio/core-bridge": ["@temporalio/core-bridge@1.20.3", "", { "dependencies": { "@grpc/grpc-js": "^1.12.4", "@temporalio/common": "1.20.3" } }, "sha512-i7hu6lg6bm2esyYYDwHh0XmGkOWSL5iicLjC7DNRU4b4As/dKcRxYMxNY6dXtKzbM94xhmX6Fh4j5/fxen/4kw=="],
"@temporalio/nexus": ["@temporalio/nexus@1.20.3", "", { "dependencies": { "@temporalio/client": "1.20.3", "@temporalio/common": "1.20.3", "@temporalio/proto": "1.20.3", "long": "^5.2.3", "nexus-rpc": "^0.0.2" } }, "sha512-OSl73enJ9M8OMk2QaGHr4cQ1DDLEkjMhU1Z3tBt2zbWkKDs2IHKkl1wU/rMeXg7AJf5uqHcp209SBaDN/rqDiA=="],
"@temporalio/proto": ["@temporalio/proto@1.20.3", "", { "dependencies": { "long": "^5.2.3", "protobufjs": "^7.6.4" } }, "sha512-RdbHC3zH+N0QqG1Q38h2LilFIQ3lbGLVmLYCyNcV+PPfzrtrZ8pwBACpSGwEuF7WWFziLAhJ2oMVXTnmXY5ocQ=="],
"@temporalio/worker": ["@temporalio/worker@1.20.3", "", { "dependencies": { "@grpc/grpc-js": "^1.12.4", "@swc/core": "^1.3.102", "@temporalio/activity": "1.20.3", "@temporalio/client": "1.20.3", "@temporalio/common": "1.20.3", "@temporalio/core-bridge": "1.20.3", "@temporalio/nexus": "1.20.3", "@temporalio/proto": "1.20.3", "@temporalio/workflow": "1.20.3", "heap-js": "^2.6.0", "memfs": "^4.6.0", "nexus-rpc": "^0.0.2", "protobufjs": "^7.6.4", "rxjs": "^7.8.1", "source-map": "^0.7.4", "source-map-loader": "^5.0.0", "supports-color": "^8.1.1", "swc-loader": "^0.2.3", "unionfs": "^4.5.1", "webpack": "^5.108.4" } }, "sha512-29W39KxGoz8QfiELT5al1jr+nnZEnpWCjr5gYT2a6ZlXr2WKxbJw4rRBy+OyGwHs5kGT0pPYayYvwcDTvWRpQQ=="],
"@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=="],
"@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=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/luxon": ["@types/luxon@3.7.2", "", {}, "sha512-gW+Oib+vUtGJBtNC8V9Reww0oIpusw+4m81uncg9REGZAJfqOQHfo/nkabnc7w0QReXyPqjrbWMJk6NuAkiX3Q=="],
"@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
"@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
"@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="],
"@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="],
"@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="],
"@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="],
"@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="],
"@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="],
"@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="],
"@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="],
"@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="],
"@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="],
"@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="],
"@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="],
"@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="],
"@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="],
"@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="],
"@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
"acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="],
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.43", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ=="],
"browserslist": ["browserslist@4.28.6", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", "electron-to-chromium": "^1.5.389", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="],
"chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"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=="],
"enhanced-resolve": ["enhanced-resolve@5.24.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw=="],
"es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-uri": ["fast-uri@3.1.3", "", {}, "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg=="],
"fs-monkey": ["fs-monkey@1.1.0", "", {}, "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw=="],
"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=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"heap-js": ["heap-js@2.7.1", "", {}, "sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA=="],
"hyperdyperid": ["hyperdyperid@1.2.0", "", {}, "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"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=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
"memfs": ["memfs@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.64.0", "@jsonjoy.com/fs-fsa": "4.64.0", "@jsonjoy.com/fs-node": "4.64.0", "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-to-fsa": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "@jsonjoy.com/fs-print": "4.64.0", "@jsonjoy.com/fs-snapshot": "4.64.0", "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", "thingies": "^2.5.0", "tree-dump": "^1.0.3", "tslib": "^2.0.0" } }, "sha512-Kw72fgY7Wn+sD8KmtNWSafl1dz0UvAsE/PHs3YVfLiaZuA3HxNm9sRLqAu0ATiBGJvME1PxZXbBZPv5GycDeAw=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"minimizer-webpack-plugin": ["minimizer-webpack-plugin@5.6.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw=="],
"ms": ["ms@3.0.0-canary.1", "", {}, "sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"nexus-rpc": ["nexus-rpc@0.0.2", "", {}, "sha512-IWjIExdVYlmwXuzHdY/Q3lXCv1gbqoAXPazQhy2w4Xgtgha3H0OOujEESVPQcFUFMWm+pAk2gKnb57g8S41JZg=="],
"node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"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=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"source-map-loader": ["source-map-loader@5.0.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "source-map-js": "^1.0.2" }, "peerDependencies": { "webpack": "^5.72.1" } }, "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"swc-loader": ["swc-loader@0.2.7", "", { "dependencies": { "@swc/counter": "^0.1.3" }, "peerDependencies": { "@swc/core": "^1.2.147", "webpack": ">=2" } }, "sha512-nwYWw3Fh9ame3Rtm7StS9SBLpHRRnYcK7bnpF3UKZmesAK0gw2/ADvlURFAINmPvKtDLzp+GBiP9yLoEjg6S9w=="],
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"terser": ["terser@5.49.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA=="],
"thingies": ["thingies@2.6.0", "", { "peerDependencies": { "tslib": "^2" } }, "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg=="],
"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=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
"unionfs": ["unionfs@4.6.0", "", { "dependencies": { "fs-monkey": "^1.0.0" } }, "sha512-fJAy3gTHjFi5S3TP5EGdjs/OUMFFvI/ady3T8qVuZfkv8Qi8prV/Q8BuFEgODJslhZTT2z2qdD2lGdee9qjEnA=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="],
"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=="],
"webpack-sources": ["webpack-sources@3.5.1", "", {}, "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack": ["@jsonjoy.com/json-pack@17.67.0", "", { "dependencies": { "@jsonjoy.com/base64": "17.67.0", "@jsonjoy.com/buffers": "17.67.0", "@jsonjoy.com/codegen": "17.67.0", "@jsonjoy.com/json-pointer": "17.67.0", "@jsonjoy.com/util": "17.67.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w=="],
"@jsonjoy.com/fs-snapshot/@jsonjoy.com/util": ["@jsonjoy.com/util@17.67.0", "", { "dependencies": { "@jsonjoy.com/buffers": "17.67.0", "@jsonjoy.com/codegen": "17.67.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew=="],
"@jsonjoy.com/json-pack/@jsonjoy.com/buffers": ["@jsonjoy.com/buffers@1.2.1", "", { "peerDependencies": { "tslib": "2" } }, "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA=="],
"@jsonjoy.com/util/@jsonjoy.com/buffers": ["@jsonjoy.com/buffers@1.2.1", "", { "peerDependencies": { "tslib": "2" } }, "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA=="],
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack/@jsonjoy.com/base64": ["@jsonjoy.com/base64@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw=="],
"@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack/@jsonjoy.com/codegen": ["@jsonjoy.com/codegen@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q=="],
"@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack/@jsonjoy.com/json-pointer": ["@jsonjoy.com/json-pointer@17.67.0", "", { "dependencies": { "@jsonjoy.com/util": "17.67.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA=="],
"@jsonjoy.com/fs-snapshot/@jsonjoy.com/util/@jsonjoy.com/codegen": ["@jsonjoy.com/codegen@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q=="],
}
}
+94 -3
View File
@@ -74,27 +74,118 @@ ranking:
records:
publicLimit: 20
temporal:
addressEnv: APISTATE_TEMPORAL_ADDRESS
namespace: unidesk
taskQueue: apistate
scoreScheduleWorkflowId: apistate-score-refresh-schedule
workflowExecutionTimeout: 5m
activityStartToCloseTimeout: 4m
retry:
maximumAttempts: 1
runtime:
secretsRoot: /root/.unidesk/.state/secrets
secretSourcePaths:
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
overApiTarget: native-api
cliTargets:
local:
mode: embedded
databasePath: .state/sub2rank.sqlite
scoreCachePath: .state/account-scores.json
production:
mode: http
baseUrl: https://apistate.hwpod.com
adminToken:
sourceRef: platform-infra/apistate.env
sourceKey: APISTATE_API_KEY
native-api:
mode: http
baseUrl: http://127.0.0.1:8080
adminToken:
sourceRef: platform-infra/apistate.env
sourceKey: APISTATE_API_KEY
native:
stateDir: .state/native
env:
APISTATE_WEB_PASSWORD:
sourceRef: platform-infra/apistate.env
sourceKey: APISTATE_WEB_PASSWORD
APISTATE_API_KEY:
sourceRef: platform-infra/apistate.env
sourceKey: APISTATE_API_KEY
APISTATE_SESSION_SECRET:
sourceRef: platform-infra/apistate.env
sourceKey: APISTATE_SESSION_SECRET
SUB2API_ADMIN_EMAIL:
sourceRef: platform-infra/sub2api.env
sourceKey: ADMIN_EMAIL
SUB2API_ADMIN_PASSWORD:
sourceRef: platform-infra/sub2api.env
sourceKey: ADMIN_PASSWORD
services:
api:
command:
- bun
- --hot
- src/api.ts
- --config
- config/sub2rank.yaml
- --runtime
- native
pidFile: api.pid
logFile: api.log
worker:
command:
- bun
- src/worker.ts
- --config
- config/sub2rank.yaml
- --runtime
- native
pidFile: worker.pid
logFile: worker.log
web:
command:
- bun
- --hot
- src/dev-web.ts
- --config
- config/sub2rank.yaml
- --runtime
- native
pidFile: web.pid
logFile: web.log
serverTargets:
k8s:
listenHost: 0.0.0.0
native:
listenHost: 127.0.0.1
listenPort: 8080
databasePath: /var/lib/sub2rank/sub2rank.sqlite
workerHealthHost: 127.0.0.1
workerHealthPort: 8081
webListenHost: 127.0.0.1
webListenPort: 5173
webApiBaseUrl: http://127.0.0.1:8080
databasePath: .state/sub2rank.sqlite
scoreCachePath: .state/account-scores.json
adminTokenEnv: APISTATE_API_KEY
sub2apiAdminEmailEnv: SUB2API_ADMIN_EMAIL
sub2apiAdminPasswordEnv: SUB2API_ADMIN_PASSWORD
webPasswordEnv: APISTATE_WEB_PASSWORD
apiKeyEnv: APISTATE_API_KEY
sessionSecretEnv: APISTATE_SESSION_SECRET
k8s:
listenHost: 0.0.0.0
listenPort: 8080
workerHealthHost: 0.0.0.0
workerHealthPort: 8081
webListenHost: 127.0.0.1
webListenPort: 5173
webApiBaseUrl: http://127.0.0.1:8080
databasePath: /var/lib/sub2rank/sub2rank.sqlite
scoreCachePath: /var/lib/sub2rank/account-scores.json
adminTokenEnv: APISTATE_API_KEY
sub2apiAdminEmailEnv: SUB2API_ADMIN_EMAIL
sub2apiAdminPasswordEnv: SUB2API_ADMIN_PASSWORD
+12 -2
View File
@@ -5,10 +5,16 @@
"type": "module",
"scripts": {
"cli": "bun scripts/apistate-cli.ts --config config/sub2rank.yaml",
"server": "bun src/server.ts --config config/sub2rank.yaml --runtime k8s",
"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",
"server": "bun src/api.ts --config config/sub2rank.yaml --runtime k8s",
"check": "bunx tsc --noEmit"
},
"dependencies": {
"@temporalio/client": "^1.20.3",
"@temporalio/worker": "^1.20.3",
"@temporalio/workflow": "^1.20.3",
"luxon": "^3.7.2",
"yaml": "^2.8.1"
},
@@ -16,5 +22,9 @@
"@types/bun": "^1.2.21",
"@types/luxon": "^3.7.1",
"typescript": "^5.9.2"
}
},
"trustedDependencies": [
"@swc/core",
"protobufjs"
]
}
+116 -108
View File
@@ -1,7 +1,12 @@
import { AdminHttpClient } from "../../src/admin-http-client";
import { mergeAccountScores } from "../../src/account-score-aggregation";
import { createEmbeddedContext } from "../../src/bootstrap";
import { loadConfig, type EmbeddedCliTarget, type HttpCliTarget } from "../../src/config";
import { loadConfig, type EmbeddedCliTarget, type HttpCliTarget, type NativeServiceId } from "../../src/config";
import type { AppCommand } from "../../src/contracts";
import { usesWorkflow } from "../../src/contracts";
import { ApplicationDispatcher } from "../../src/dispatcher";
import { nativeLogs, nativeStart, nativeStatus, nativeStop } from "../../src/native-services";
import { TemporalGateway } from "../../src/temporal-client";
interface Parsed {
configPath: string;
@@ -9,9 +14,13 @@ interface Parsed {
command: string[];
confirm: boolean;
includeRecords: boolean;
overApi: boolean;
json: boolean;
id: string | null;
component: NativeServiceId | null;
limit: number | null;
draws: number | null;
tail: number | null;
}
function record(value: unknown): Record<string, unknown> | null {
@@ -29,8 +38,8 @@ function value(args: string[], name: string): string | null {
function parseArgs(args: string[]): Parsed {
const configPath = value(args, "--config");
if (!configPath) throw new Error("--config is required");
const optionNames = new Set(["--config", "--target", "--id", "--limit", "--draws"]);
const flags = new Set(["--confirm", "--include-records"]);
const optionNames = new Set(["--config", "--target", "--id", "--limit", "--draws", "--component", "--tail"]);
const flags = new Set(["--confirm", "--include-records", "--over-api", "--json"]);
const command: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const item = args[index]!;
@@ -46,56 +55,90 @@ function parseArgs(args: string[]): Parsed {
if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${name} must be a non-negative integer`);
return parsed;
};
const component = value(args, "--component");
if (component !== null && component !== "api" && component !== "worker" && component !== "web") throw new Error("--component must be api, worker, or web");
return {
configPath,
targetId: value(args, "--target"),
command,
confirm: args.includes("--confirm"),
includeRecords: args.includes("--include-records"),
overApi: args.includes("--over-api"),
json: args.includes("--json"),
id: value(args, "--id"),
component,
limit: integer("--limit"),
draws: integer("--draws"),
tail: integer("--tail"),
};
}
function help(): Record<string, unknown> {
return {
ok: true,
usage: "bun scripts/apistate-cli.ts --config config/sub2rank.yaml [--target local|production] <command>",
usage: "bun scripts/apistate-cli.ts --config config/sub2rank.yaml [--over-api] [--target <id>] <command>",
commands: [
"config validate",
"backend check",
"scores aggregate-smoke",
"lottery status",
"lottery draw [--confirm]",
"lottery reset [--draws N] [--include-records] --confirm",
"records list [--limit N]",
"records delete --id <record-id> --confirm",
"credit test [--confirm]",
"api smoke",
"scores get|refresh|aggregate-smoke",
"lottery status|draw|reset",
"records list|delete",
"credit test",
"api smoke --over-api",
"native start|stop|status|logs --component api|worker|web [--tail N]",
],
output: "k8s-style text by default; add --json for machine output",
};
}
function emit(value: Record<string, unknown>, json: boolean): void {
if (json) {
console.log(JSON.stringify(value, null, 2));
return;
}
console.log("APISTATE RESULT");
console.log("FIELD VALUE");
console.log("-------------------- ------------------------------------------------------------");
for (const [key, item] of Object.entries(value)) {
const rendered = item === null || item === undefined ? "-" : typeof item === "object" ? JSON.stringify(item) : String(item);
console.log(`${key.padEnd(20)} ${rendered}`);
}
}
function appCommand(parsed: Parsed, config: ReturnType<typeof loadConfig>): AppCommand | Record<string, unknown> {
const [group, action] = parsed.command;
if (group === "backend" && action === "check") return { kind: "backend.check" };
if (group === "scores" && action === "get") return { kind: "scores.get" };
if (group === "scores" && action === "refresh") return { kind: "scores.refresh" };
if (group === "lottery" && action === "status") return { kind: "lottery.status" };
if (group === "lottery" && action === "draw") return parsed.confirm ? { kind: "lottery.draw" } : { ok: true, mutation: false, action: "lottery-draw", hint: "add --confirm to execute" };
if (group === "lottery" && action === "reset") {
const draws = parsed.draws ?? config.lottery.initialDrawCount;
return parsed.confirm ? { kind: "lottery.reset", draws, includeRecords: parsed.includeRecords } : { ok: true, mutation: false, action: "lottery-reset", draws, includeRecords: parsed.includeRecords, hint: "add --confirm to execute" };
}
if (group === "records" && action === "list") return { kind: "records.list", limit: parsed.limit ?? config.records.publicLimit };
if (group === "records" && action === "delete") {
if (!parsed.id) throw new Error("records delete requires --id");
return parsed.confirm ? { kind: "records.delete", id: parsed.id } : { ok: true, mutation: false, action: "record-delete", id: parsed.id, hint: "add --confirm to execute" };
}
if (group === "credit" && action === "test") return { kind: "credit.test", execute: parsed.confirm };
throw new Error(`unknown command: ${parsed.command.join(" ")}`);
}
function isAppCommand(value: AppCommand | Record<string, unknown>): value is AppCommand {
return typeof value.kind === "string";
}
async function embedded(parsed: Parsed, config: ReturnType<typeof loadConfig>, target: EmbeddedCliTarget): Promise<unknown> {
const command = appCommand(parsed, config);
if (!isAppCommand(command)) return command;
const context = createEmbeddedContext(config, target);
let temporal: TemporalGateway | null = null;
try {
const [group, action] = parsed.command;
if (group === "backend" && action === "check") return await context.service.status(true);
if (group === "lottery" && action === "status") return await context.service.status(false);
if (group === "lottery" && action === "draw") return parsed.confirm ? { ok: true, record: await context.service.draw() } : { ok: true, mutation: false, action: "lottery-draw", hint: "add --confirm to execute" };
if (group === "lottery" && action === "reset") {
const draws = parsed.draws ?? config.lottery.initialDrawCount;
return parsed.confirm ? context.service.reset(draws, parsed.includeRecords) : { ok: true, mutation: false, action: "lottery-reset", draws, includeRecords: parsed.includeRecords, hint: "add --confirm to execute" };
}
if (group === "records" && action === "list") return { ok: true, records: context.service.listRecords(parsed.limit ?? config.records.publicLimit) };
if (group === "records" && action === "delete") {
if (!parsed.id) throw new Error("records delete requires --id");
return parsed.confirm ? context.service.deleteRecord(parsed.id) : { ok: true, mutation: false, action: "record-delete", id: parsed.id, hint: "add --confirm to execute" };
}
if (group === "credit" && action === "test") return await context.service.creditTest(parsed.confirm);
throw new Error(`unknown command: ${parsed.command.join(" ")}`);
if (usesWorkflow(command)) temporal = await TemporalGateway.connect(config);
return await new ApplicationDispatcher({ lottery: context.service, scores: context.monitor }, temporal).dispatch(command);
} finally {
if (temporal) await temporal.close();
context.close();
}
}
@@ -104,6 +147,8 @@ async function remote(parsed: Parsed, config: ReturnType<typeof loadConfig>, tar
const client = new AdminHttpClient(config, target);
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 === "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") {
@@ -117,105 +162,68 @@ async function remote(parsed: Parsed, config: ReturnType<typeof loadConfig>, tar
}
if (group === "credit" && action === "test") return await client.creditTest(parsed.confirm);
if (group === "api" && action === "smoke") {
const [status, scores, ranking, lottery] = await Promise.all([
client.serviceStatus(),
client.scores(),
client.ranking(),
client.lottery(),
]);
const [status, scores, ranking, lottery] = await Promise.all([client.serviceStatus(), client.scores(), client.ranking(), client.lottery()]);
const refreshed = await client.refreshScores();
return {
ok: true,
ok: status.ok === true && scores.ok === true && refreshed.ok === true && ranking.ok === true && lottery.ok === true,
action: "apistate-api-smoke",
checks: {
status: status.ok === true,
scores: scores.ok === true,
refresh: refreshed.ok === true && refreshed.snapshotOk === true,
ranking: ranking.ok === true,
lottery: lottery.ok === true,
},
checks: { status: status.ok === true, scores: scores.ok === true, refresh: refreshed.ok === true && refreshed.snapshotOk === true, ranking: ranking.ok === true, lottery: lottery.ok === true },
valuesPrinted: false,
};
}
throw new Error(`unknown command: ${parsed.command.join(" ")}`);
}
function aggregateSmoke(): Record<string, unknown> {
const account = (groupId: number, groupName: string, successRequests: number, failureRequests: number, ttftP95Ms: number, apiAmountUsd: number) => ({
accountId: 15, accountName: "lyon9801 0.0", groupId, groupName, status: "active", currentlyAvailable: true, priority: 1,
successRequests, failureRequests, observedAttempts: successRequests + failureRequests, streamSuccessRequests: successRequests,
firstTokenSamples: successRequests, ttftP95Ms, usage: { requestCount: successRequests, tokenCount: successRequests * 1000, apiAmountUsd },
});
const rows = mergeAccountScores([account(3, "自用", 100, 0, 10_591, 1), account(2, "unidesk-codex-pool", 50, 1, 13_206, 0.5)]);
const row = rows[0] ?? {};
const checks = {
uniqueAccount: rows.length === 1,
groupsMerged: Array.isArray(row.groupNames) && row.groupNames.length === 2,
attemptsMerged: row.observedAttempts === 151,
usageMerged: record(row.usage)?.requestCount === 150 && record(row.usage)?.apiAmountUsd === 1.5,
decimalScore: typeof row.score === "number" && !Number.isInteger(row.score),
conservativeTtft: row.ttftP95Ms === 13_206,
};
return { ok: Object.values(checks).every(Boolean), action: "account-score-aggregate-smoke", checks, mutation: false };
}
export async function runCli(args: string[]): Promise<void> {
const wantsJson = args.includes("--json");
try {
if (args.includes("--help") || args.length === 0) {
console.log(JSON.stringify(help(), null, 2));
return;
}
if (args.includes("--help") || args.length === 0) return emit(help(), wantsJson);
const parsed = parseArgs(args);
const config = loadConfig(parsed.configPath);
if (parsed.command.join(" ") === "config validate") {
console.log(JSON.stringify({
ok: true,
configPath: config.configPath,
kind: config.kind,
service: config.metadata.name,
refreshIntervalMinutes: config.monitor.refreshIntervalMinutes,
scoreWindow: config.monitor.scoreWindow,
automaticCreditEnabled: config.lottery.automaticCredit.enabled,
excludedIdentities: config.lottery.eligibility.excludedIdentities,
valuesPrinted: false,
}, null, 2));
return;
if (parsed.command.join(" ") === "config validate") return emit({
ok: true, configPath: config.configPath, kind: config.kind, service: config.metadata.name,
temporalNamespace: config.temporal.namespace, temporalTaskQueue: config.temporal.taskQueue,
refreshIntervalMinutes: config.monitor.refreshIntervalMinutes, scoreWindow: config.monitor.scoreWindow,
automaticCreditEnabled: config.lottery.automaticCredit.enabled, valuesPrinted: false,
}, parsed.json);
if (parsed.command.join(" ") === "scores aggregate-smoke") return emit(aggregateSmoke(), parsed.json);
if (parsed.command[0] === "native") {
const action = parsed.command[1];
if (!parsed.component) throw new Error("native commands require --component api, worker, or web");
const result = action === "start" ? nativeStart(config, parsed.component)
: action === "stop" ? nativeStop(config, parsed.component)
: action === "status" ? nativeStatus(config, parsed.component)
: action === "logs" ? nativeLogs(config, parsed.component, parsed.tail ?? 40)
: (() => { throw new Error(`unknown native action ${action ?? ""}`); })();
return emit(result, parsed.json);
}
if (parsed.command.join(" ") === "scores aggregate-smoke") {
const account = (groupId: number, groupName: string, successRequests: number, failureRequests: number, ttftP95Ms: number, apiAmountUsd: number) => ({
accountId: 15,
accountName: "lyon9801 0.0",
groupId,
groupName,
status: "active",
currentlyAvailable: true,
priority: 1,
successRequests,
failureRequests,
observedAttempts: successRequests + failureRequests,
streamSuccessRequests: successRequests,
firstTokenSamples: successRequests,
ttftP95Ms,
usage: { requestCount: successRequests, tokenCount: successRequests * 1000, apiAmountUsd },
});
const rows = mergeAccountScores([
account(3, "自用", 100, 0, 10_591, 1),
account(2, "unidesk-codex-pool", 50, 1, 13_206, 0.5),
]);
const row = rows[0] ?? {};
const checks = {
uniqueAccount: rows.length === 1,
groupsMerged: Array.isArray(row.groupNames) && row.groupNames.length === 2,
attemptsMerged: row.observedAttempts === 151,
usageMerged: record(row.usage)?.requestCount === 150 && record(row.usage)?.apiAmountUsd === 1.5,
decimalScore: typeof row.score === "number" && !Number.isInteger(row.score),
conservativeTtft: row.ttftP95Ms === 13_206,
};
console.log(JSON.stringify({
ok: Object.values(checks).every(Boolean),
action: "account-score-aggregate-smoke",
checks,
result: {
accountId: row.accountId,
accountName: row.accountName,
groupNames: row.groupNames,
observedAttempts: row.observedAttempts,
score: row.score,
ttftP95Ms: row.ttftP95Ms,
usage: row.usage,
},
mutation: false,
}, null, 2));
return;
}
const targetId = parsed.targetId ?? config.runtime.defaultCliTarget;
const targetId = parsed.targetId ?? (parsed.overApi ? config.runtime.overApiTarget : config.runtime.defaultCliTarget);
const target = config.runtime.cliTargets[targetId];
if (!target) throw new Error(`runtime.cliTargets.${targetId} does not exist`);
if (parsed.overApi && target.mode !== "http") throw new Error(`--over-api requires an http target; ${targetId} is ${target.mode}`);
const result = target.mode === "embedded" ? await embedded(parsed, config, target) : await remote(parsed, config, target);
console.log(JSON.stringify({ target: targetId, ...result as Record<string, unknown> }, null, 2));
emit({ target: targetId, transport: target.mode === "embedded" ? "local-dispatcher" : "http", ...result as Record<string, unknown> }, parsed.json);
} catch (error) {
console.error(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }, null, 2));
emit({ ok: false, error: error instanceof Error ? error.message : String(error), valuesPrinted: false }, wantsJson);
process.exitCode = 1;
}
}
+45
View File
@@ -0,0 +1,45 @@
import { createServerContext } from "./bootstrap";
import { loadConfig } from "./config";
import { ApplicationDispatcher } from "./dispatcher";
import { createHandler } from "./http";
import { requiredOption } from "./runtime-args";
import { TemporalGateway } from "./temporal-client";
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 adminToken = process.env[target.adminTokenEnv];
if (!adminToken) throw new Error(`server target requires env ${target.adminTokenEnv}`);
const context = createServerContext(config, target);
const temporal = await TemporalGateway.connect(config);
const dispatcher = new ApplicationDispatcher({ lottery: context.service, scores: context.monitor }, temporal);
const server = Bun.serve({
hostname: target.listenHost,
port: target.listenPort,
fetch: createHandler(dispatcher, config, context.auth, adminToken),
});
console.log(JSON.stringify({
ok: true,
component: "apistate-api",
runtime: runtimeId,
listen: server.url.toString(),
temporalNamespace: config.temporal.namespace,
temporalTaskQueue: config.temporal.taskQueue,
automaticCreditEnabled: config.lottery.automaticCredit.enabled,
valuesPrinted: false,
}));
let stopping = false;
async function stop(): Promise<void> {
if (stopping) return;
stopping = true;
server.stop(true);
context.close();
await temporal.close();
process.exit(0);
}
for (const signal of ["SIGINT", "SIGTERM"] as const) process.on(signal, () => void stop());
+2 -2
View File
@@ -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, ".state/account-scores.json"));
const monitor = new AccountScoreService(config, resolveDataPath(config, target.scoreCachePath));
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, "account-scores.json"));
const monitor = new AccountScoreService(config, resolveDataPath(config, target.scoreCachePath));
return {
service: new LotteryService(config, store, client),
store,
+80 -1
View File
@@ -50,11 +50,26 @@ export interface AppConfig {
};
ranking: { timezone: string; windowDays: number; sourceLimit: number; displayLimit: number };
records: { publicLimit: number };
temporal: {
addressEnv: string;
namespace: string;
taskQueue: string;
scoreScheduleWorkflowId: string;
workflowExecutionTimeout: string;
activityStartToCloseTimeout: string;
retry: { maximumAttempts: number };
};
runtime: {
secretsRoot: string;
secretSourcePaths: Record<string, string>;
defaultCliTarget: string;
overApiTarget: string;
cliTargets: Record<string, EmbeddedCliTarget | HttpCliTarget>;
native: {
stateDir: string;
env: Record<string, SecretRef>;
services: Record<NativeServiceId, NativeServiceConfig>;
};
serverTargets: Record<string, ServerTarget>;
};
configPath: string;
@@ -64,6 +79,7 @@ export interface AppConfig {
export interface EmbeddedCliTarget {
mode: "embedded";
databasePath: string;
scoreCachePath: string;
}
export interface HttpCliTarget {
@@ -75,7 +91,13 @@ export interface HttpCliTarget {
export interface ServerTarget {
listenHost: string;
listenPort: number;
workerHealthHost: string;
workerHealthPort: number;
webListenHost: string;
webListenPort: number;
webApiBaseUrl: string;
databasePath: string;
scoreCachePath: string;
adminTokenEnv: string;
sub2apiAdminEmailEnv: string;
sub2apiAdminPasswordEnv: string;
@@ -84,6 +106,14 @@ export interface ServerTarget {
sessionSecretEnv: string;
}
export type NativeServiceId = "api" | "worker" | "web";
export interface NativeServiceConfig {
command: string[];
pidFile: string;
logFile: string;
}
type ObjectValue = Record<string, unknown>;
function object(value: unknown, path: string): ObjectValue {
@@ -139,6 +169,12 @@ function secretRef(value: unknown, path: string): SecretRef {
return { sourceRef: stringValue(raw, "sourceRef", path), sourceKey: stringValue(raw, "sourceKey", path) };
}
function nativeFile(parent: ObjectValue, key: string, path: string): string {
const value = stringValue(parent, key, path);
if (value.includes("/") || value.includes("\\") || value === "." || value === "..") throw new Error(`${path}.${key} must be a filename`);
return value;
}
export function loadConfig(path: string): AppConfig {
const configPath = resolve(path);
const rootDirectory = resolve(dirname(configPath), "..");
@@ -157,7 +193,12 @@ export function loadConfig(path: string): AppConfig {
const creditTest = object(lottery.creditTest, "lottery.creditTest");
const ranking = object(raw.ranking, "ranking");
const records = object(raw.records, "records");
const temporal = object(raw.temporal, "temporal");
const temporalRetry = object(temporal.retry, "temporal.retry");
const runtime = object(raw.runtime, "runtime");
const native = object(runtime.native, "runtime.native");
const nativeServicesRaw = object(native.services, "runtime.native.services");
const nativeEnvRaw = object(native.env, "runtime.native.env");
const secretSourcePathsRaw = object(runtime.secretSourcePaths, "runtime.secretSourcePaths");
const cliTargetsRaw = object(runtime.cliTargets, "runtime.cliTargets");
const serverTargetsRaw = object(runtime.serverTargets, "runtime.serverTargets");
@@ -165,7 +206,11 @@ export function loadConfig(path: string): AppConfig {
for (const [id, value] of Object.entries(cliTargetsRaw)) {
const target = object(value, `runtime.cliTargets.${id}`);
const mode = stringValue(target, "mode", `runtime.cliTargets.${id}`);
if (mode === "embedded") cliTargets[id] = { mode, databasePath: stringValue(target, "databasePath", `runtime.cliTargets.${id}`) };
if (mode === "embedded") cliTargets[id] = {
mode,
databasePath: stringValue(target, "databasePath", `runtime.cliTargets.${id}`),
scoreCachePath: stringValue(target, "scoreCachePath", `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`);
}
@@ -175,7 +220,13 @@ export function loadConfig(path: string): AppConfig {
serverTargets[id] = {
listenHost: stringValue(target, "listenHost", `runtime.serverTargets.${id}`),
listenPort: numberValue(target, "listenPort", `runtime.serverTargets.${id}`, 1),
workerHealthHost: stringValue(target, "workerHealthHost", `runtime.serverTargets.${id}`),
workerHealthPort: numberValue(target, "workerHealthPort", `runtime.serverTargets.${id}`, 1),
webListenHost: stringValue(target, "webListenHost", `runtime.serverTargets.${id}`),
webListenPort: numberValue(target, "webListenPort", `runtime.serverTargets.${id}`, 1),
webApiBaseUrl: stringValue(target, "webApiBaseUrl", `runtime.serverTargets.${id}`),
databasePath: stringValue(target, "databasePath", `runtime.serverTargets.${id}`),
scoreCachePath: stringValue(target, "scoreCachePath", `runtime.serverTargets.${id}`),
adminTokenEnv: stringValue(target, "adminTokenEnv", `runtime.serverTargets.${id}`),
sub2apiAdminEmailEnv: stringValue(target, "sub2apiAdminEmailEnv", `runtime.serverTargets.${id}`),
sub2apiAdminPasswordEnv: stringValue(target, "sub2apiAdminPasswordEnv", `runtime.serverTargets.${id}`),
@@ -188,6 +239,19 @@ export function loadConfig(path: string): AppConfig {
if (automaticMode !== "dry-run" && automaticMode !== "live") throw new Error("lottery.automaticCredit.mode must be dry-run or live");
const defaultCliTarget = stringValue(runtime, "defaultCliTarget", "runtime");
if (!cliTargets[defaultCliTarget]) throw new Error(`runtime.defaultCliTarget references missing target ${defaultCliTarget}`);
const overApiTarget = stringValue(runtime, "overApiTarget", "runtime");
if (cliTargets[overApiTarget]?.mode !== "http") throw new Error(`runtime.overApiTarget must reference an http target`);
const nativeServices = {} as Record<NativeServiceId, NativeServiceConfig>;
for (const id of ["api", "worker", "web"] as const) {
const service = object(nativeServicesRaw[id], `runtime.native.services.${id}`);
const command = strings(service, "command", `runtime.native.services.${id}`);
if (command.length === 0) throw new Error(`runtime.native.services.${id}.command must not be empty`);
nativeServices[id] = {
command,
pidFile: nativeFile(service, "pidFile", `runtime.native.services.${id}`),
logFile: nativeFile(service, "logFile", `runtime.native.services.${id}`),
};
}
return {
apiVersion: stringValue(raw, "apiVersion", "config"),
kind: stringValue(raw, "kind", "config"),
@@ -255,6 +319,15 @@ export function loadConfig(path: string): AppConfig {
displayLimit: integerValue(ranking, "displayLimit", "ranking", 1),
},
records: { publicLimit: integerValue(records, "publicLimit", "records", 1) },
temporal: {
addressEnv: stringValue(temporal, "addressEnv", "temporal"),
namespace: stringValue(temporal, "namespace", "temporal"),
taskQueue: stringValue(temporal, "taskQueue", "temporal"),
scoreScheduleWorkflowId: stringValue(temporal, "scoreScheduleWorkflowId", "temporal"),
workflowExecutionTimeout: stringValue(temporal, "workflowExecutionTimeout", "temporal"),
activityStartToCloseTimeout: stringValue(temporal, "activityStartToCloseTimeout", "temporal"),
retry: { maximumAttempts: integerValue(temporalRetry, "maximumAttempts", "temporal.retry", 1) },
},
runtime: {
secretsRoot: stringValue(runtime, "secretsRoot", "runtime"),
secretSourcePaths: Object.fromEntries(Object.entries(secretSourcePathsRaw).map(([ref, value]) => {
@@ -262,7 +335,13 @@ export function loadConfig(path: string): AppConfig {
return [ref, value];
})),
defaultCliTarget,
overApiTarget,
cliTargets,
native: {
stateDir: stringValue(native, "stateDir", "runtime.native"),
env: Object.fromEntries(Object.entries(nativeEnvRaw).map(([targetKey, value]) => [targetKey, secretRef(value, `runtime.native.env.${targetKey}`)])),
services: nativeServices,
},
serverTargets,
},
configPath,
+36
View File
@@ -0,0 +1,36 @@
export type AppCommand =
| { kind: "backend.check" }
| { kind: "scores.get" }
| { kind: "scores.refresh" }
| { kind: "ranking.get" }
| { kind: "lottery.publicState" }
| { kind: "lottery.publicDraw" }
| { kind: "lottery.status" }
| { kind: "lottery.draw" }
| { kind: "lottery.reset"; draws: number; includeRecords: boolean }
| { kind: "records.list"; limit: number }
| { kind: "records.delete"; id: string }
| { kind: "credit.test"; execute: boolean };
export interface OperationRequest {
operationId: string;
command: AppCommand;
}
export interface WorkflowOptions {
activityStartToCloseTimeout: string;
maximumAttempts: number;
}
export interface ScheduledScoreRefreshInput extends WorkflowOptions {
intervalMs: number;
}
export function usesWorkflow(command: AppCommand): boolean {
return command.kind === "scores.refresh"
|| command.kind === "lottery.publicDraw"
|| command.kind === "lottery.draw"
|| command.kind === "lottery.reset"
|| command.kind === "records.delete"
|| (command.kind === "credit.test" && command.execute);
}
+56
View File
@@ -0,0 +1,56 @@
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<string, string> = {
".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<string, string>)[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);
});
+46
View File
@@ -0,0 +1,46 @@
import type { AppCommand, OperationRequest } from "./contracts";
import type { AccountScoreService } from "./account-score-service";
import type { LotteryService } from "./lottery-service";
import type { TemporalGateway } from "./temporal-client";
import { usesWorkflow } from "./contracts";
export interface DispatcherServices {
lottery: LotteryService;
scores: AccountScoreService;
}
export async function dispatchDirect(services: DispatcherServices, command: AppCommand): Promise<unknown> {
if (command.kind === "backend.check") return await services.lottery.status(true);
if (command.kind === "scores.get") return services.scores.state();
if (command.kind === "scores.refresh") return await services.scores.refresh();
if (command.kind === "ranking.get") return await services.lottery.ranking();
if (command.kind === "lottery.publicState") return await services.lottery.publicState();
if (command.kind === "lottery.publicDraw") return await services.lottery.publicDraw();
if (command.kind === "lottery.status") return await services.lottery.status(false);
if (command.kind === "lottery.draw") return { ok: true, record: await services.lottery.draw() };
if (command.kind === "lottery.reset") return services.lottery.reset(command.draws, command.includeRecords);
if (command.kind === "records.list") return { ok: true, records: services.lottery.listRecords(command.limit) };
if (command.kind === "records.delete") return services.lottery.deleteRecord(command.id);
if (command.kind === "credit.test") return await services.lottery.creditTest(command.execute);
const exhaustive: never = command;
throw new Error(`unsupported command ${JSON.stringify(exhaustive)}`);
}
export class ApplicationDispatcher {
constructor(
private readonly services: DispatcherServices,
private readonly temporal: TemporalGateway | null,
) {}
async dispatch(command: AppCommand): Promise<unknown> {
if (!usesWorkflow(command)) return await dispatchDirect(this.services, command);
if (!this.temporal) throw new Error(`command ${command.kind} requires Temporal`);
return await this.temporal.execute(command);
}
}
export function createActivities(services: DispatcherServices): { executeOperation(request: OperationRequest): Promise<unknown> } {
return {
executeOperation: async (request) => await dispatchDirect(services, request.command),
};
}
+17 -19
View File
@@ -1,7 +1,6 @@
import { resolve } from "node:path";
import type { AccountScoreService } from "./account-score-service";
import type { AppConfig } from "./config";
import type { LotteryService } from "./lottery-service";
import type { ApplicationDispatcher } from "./dispatcher";
import {
apiKeyAuthorized,
clearSessionCookie,
@@ -44,8 +43,7 @@ async function staticFile(name: string, contentType: string): Promise<Response>
}
export function createHandler(
service: LotteryService,
monitor: AccountScoreService,
dispatcher: ApplicationDispatcher,
config: AppConfig,
auth: WebAuthSecrets,
legacyAdminToken: string,
@@ -56,8 +54,8 @@ export function createHandler(
const apiKey = apiKeyAuthorized(request, auth) || request.headers.get("authorization") === `Bearer ${legacyAdminToken}`;
try {
if (request.method === "GET" && url.pathname === "/health") {
const scores = monitor.state();
return json({ ok: true, service: "apistate", scoreStatus: scores.status, refreshedAt: scores.refreshedAt });
const scores = await dispatcher.dispatch({ kind: "scores.get" }) as Record<string, unknown>;
return json({ ok: true, service: "apistate-api", scoreStatus: scores.status, refreshedAt: scores.refreshedAt });
}
if (request.method === "POST" && url.pathname === "/api/login") {
const input = await body(request);
@@ -78,41 +76,41 @@ export function createHandler(
if (url.pathname.startsWith("/api/") && !session && !apiKey) return json({ ok: false, error: "unauthorized" }, 401);
if (request.method === "GET" && url.pathname === "/api/status") {
const scores = monitor.state();
const scores = await dispatcher.dispatch({ kind: "scores.get" }) as Record<string, unknown>;
return json({ ok: true, service: "apistate", scoreStatus: scores.status, refreshedAt: scores.refreshedAt, nextRefreshAt: scores.nextRefreshAt });
}
if (request.method === "GET" && url.pathname === "/api/scores") {
const state = monitor.state();
const state = await dispatcher.dispatch({ kind: "scores.get" }) as Record<string, unknown>;
return json({ ...state, snapshotOk: state.ok, ok: true });
}
if (request.method === "POST" && url.pathname === "/api/scores/refresh") {
const state = await monitor.refresh();
const state = await dispatcher.dispatch({ kind: "scores.refresh" }) as Record<string, unknown>;
return json({ ...state, snapshotOk: state.ok, ok: true });
}
if (request.method === "GET" && url.pathname === "/api/ranking") return json({ ok: true, ranking: await service.ranking() });
if (request.method === "GET" && url.pathname === "/api/lottery") return json(await service.publicState());
if (request.method === "POST" && url.pathname === "/api/lottery/draw") return json({ ok: true, record: await service.publicDraw() });
if (request.method === "GET" && url.pathname === "/api/ranking") return json({ ok: true, ranking: await dispatcher.dispatch({ kind: "ranking.get" }) });
if (request.method === "GET" && url.pathname === "/api/lottery") return json(await dispatcher.dispatch({ kind: "lottery.publicState" }));
if (request.method === "POST" && url.pathname === "/api/lottery/draw") return json({ ok: true, record: await dispatcher.dispatch({ kind: "lottery.publicDraw" }) });
if (!url.pathname.startsWith("/api/admin/")) return json({ ok: false, error: "not found" }, 404);
if (!apiKey) return json({ ok: false, error: "unauthorized" }, 401);
if (request.method === "GET" && url.pathname === "/api/admin/status") return json(await service.status(false));
if (request.method === "GET" && url.pathname === "/api/admin/backend-check") return json(await service.status(true));
if (request.method === "POST" && url.pathname === "/api/admin/draw") return json({ ok: true, record: await service.draw() });
if (request.method === "GET" && url.pathname === "/api/admin/status") return json(await dispatcher.dispatch({ kind: "lottery.status" }));
if (request.method === "GET" && url.pathname === "/api/admin/backend-check") return json(await dispatcher.dispatch({ kind: "backend.check" }));
if (request.method === "POST" && url.pathname === "/api/admin/draw") return json(await dispatcher.dispatch({ kind: "lottery.draw" }));
if (request.method === "POST" && url.pathname === "/api/admin/reset") {
const input = await body(request);
if (!Number.isInteger(input.draws) || Number(input.draws) < 0 || typeof input.includeRecords !== "boolean") return json({ ok: false, error: "draws must be a non-negative integer and includeRecords must be boolean" }, 400);
return json(service.reset(Number(input.draws), input.includeRecords));
return json(await dispatcher.dispatch({ kind: "lottery.reset", draws: Number(input.draws), includeRecords: input.includeRecords }));
}
if (request.method === "GET" && url.pathname === "/api/admin/records") {
const limit = Number(url.searchParams.get("limit"));
if (!Number.isInteger(limit) || limit < 1) return json({ ok: false, error: "limit must be a positive integer" }, 400);
return json({ ok: true, records: service.listRecords(limit) });
return json(await dispatcher.dispatch({ kind: "records.list", limit }));
}
if (request.method === "DELETE" && url.pathname.startsWith("/api/admin/records/")) return json(service.deleteRecord(decodeURIComponent(url.pathname.slice("/api/admin/records/".length))));
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/credit-test") {
const input = await body(request);
if (typeof input.execute !== "boolean") return json({ ok: false, error: "execute must be boolean" }, 400);
return json(await service.creditTest(input.execute));
return json(await dispatcher.dispatch({ kind: "credit.test", execute: input.execute }));
}
return json({ ok: false, error: "not found" }, 404);
} catch (error) {
+87
View File
@@ -0,0 +1,87 @@
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { spawn } from "node:child_process";
import { resolve } from "node:path";
import type { AppConfig, NativeServiceId } from "./config";
import { readSecret } from "./secrets";
function paths(config: AppConfig, component: NativeServiceId): { stateDir: string; pid: string; log: string } {
const service = config.runtime.native.services[component];
const stateDir = resolve(config.rootDirectory, config.runtime.native.stateDir);
return { stateDir, pid: resolve(stateDir, service.pidFile), log: resolve(stateDir, service.logFile) };
}
function processId(path: string): number | null {
if (!existsSync(path)) return null;
const pid = Number(readFileSync(path, "utf8").trim());
return Number.isInteger(pid) && pid > 0 ? pid : null;
}
function running(pid: number | null): boolean {
if (!pid) return false;
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
export function nativeStatus(config: AppConfig, component: NativeServiceId): Record<string, unknown> {
const target = paths(config, component);
const pid = processId(target.pid);
return {
ok: true,
component,
state: running(pid) ? "running" : "stopped",
pid: running(pid) ? pid : null,
pidFile: target.pid,
logFile: target.log,
valuesPrinted: false,
};
}
export function nativeStart(config: AppConfig, component: NativeServiceId): Record<string, unknown> {
const current = nativeStatus(config, component);
if (current.state === "running") return { ...current, mutation: false, reason: "already-running" };
const temporalAddress = process.env[config.temporal.addressEnv];
if ((component === "api" || component === "worker") && !temporalAddress) throw new Error(`native ${component} requires env ${config.temporal.addressEnv}`);
const service = config.runtime.native.services[component];
const target = paths(config, component);
mkdirSync(target.stateDir, { recursive: true });
rmSync(target.pid, { force: true });
const env: Record<string, string> = { ...process.env } as Record<string, string>;
for (const [targetKey, ref] of Object.entries(config.runtime.native.env)) env[targetKey] = readSecret(config, ref);
const log = openSync(target.log, "a", 0o600);
try {
const child = spawn(service.command[0]!, service.command.slice(1), {
cwd: config.rootDirectory,
env,
detached: true,
stdio: ["ignore", log, log],
});
if (!child.pid) throw new Error(`native ${component} did not return a pid`);
writeFileSync(target.pid, `${child.pid}\n`, { encoding: "utf8", mode: 0o600 });
child.unref();
return { ok: true, component, state: "starting", pid: child.pid, pidFile: target.pid, logFile: target.log, mutation: true, valuesPrinted: false };
} finally {
closeSync(log);
}
}
export function nativeStop(config: AppConfig, component: NativeServiceId): Record<string, unknown> {
const target = paths(config, component);
const pid = processId(target.pid);
if (!running(pid)) {
rmSync(target.pid, { force: true });
return { ok: true, component, state: "stopped", mutation: false, reason: "already-stopped", valuesPrinted: false };
}
process.kill(pid!, "SIGTERM");
rmSync(target.pid, { force: true });
return { ok: true, component, state: "stopping", pid, mutation: true, valuesPrinted: false };
}
export function nativeLogs(config: AppConfig, component: NativeServiceId, tail: number): Record<string, unknown> {
const target = paths(config, component);
const lines = existsSync(target.log) ? readFileSync(target.log, "utf8").split(/\r?\n/u).filter(Boolean).slice(-tail) : [];
return { ok: true, component, logFile: target.log, lines, lineCount: lines.length, mutation: false, valuesPrinted: false };
}
+6
View File
@@ -0,0 +1,6 @@
export function requiredOption(name: string): string {
const index = process.argv.indexOf(name);
const value = index >= 0 ? process.argv[index + 1] : undefined;
if (!value || value.startsWith("--")) throw new Error(`${name} is required`);
return value;
}
+2 -34
View File
@@ -1,35 +1,3 @@
import { loadConfig } from "./config";
import { createServerContext } from "./bootstrap";
import { createHandler } from "./http";
await import("./api");
function option(name: string): string {
const index = process.argv.indexOf(name);
const value = index >= 0 ? process.argv[index + 1] : undefined;
if (!value) throw new Error(`${name} is required`);
return value;
}
const config = loadConfig(option("--config"));
const runtimeId = option("--runtime");
const target = config.runtime.serverTargets[runtimeId];
if (!target) throw new Error(`runtime.serverTargets.${runtimeId} does not exist`);
const adminToken = process.env[target.adminTokenEnv];
if (!adminToken) throw new Error(`server target requires env ${target.adminTokenEnv}`);
const context = createServerContext(config, target);
const server = Bun.serve({
hostname: target.listenHost,
port: target.listenPort,
fetch: createHandler(context.service, context.monitor, config, context.auth, adminToken),
});
context.monitor.start();
console.log(JSON.stringify({ ok: true, service: config.metadata.name, runtime: runtimeId, listen: server.url.toString(), automaticCreditEnabled: config.lottery.automaticCredit.enabled }));
for (const signal of ["SIGINT", "SIGTERM"] as const) {
process.on(signal, () => {
context.close();
server.stop(true);
process.exit(0);
});
}
export {};
+60
View File
@@ -0,0 +1,60 @@
import { randomUUID } from "node:crypto";
import { Client, Connection } from "@temporalio/client";
import type { AppConfig } from "./config";
import type { AppCommand, OperationRequest } from "./contracts";
export function temporalAddress(config: AppConfig): string {
const address = process.env[config.temporal.addressEnv];
if (!address) throw new Error(`Temporal address requires env ${config.temporal.addressEnv}`);
return address;
}
export class TemporalGateway {
private constructor(
private readonly connection: Connection,
private readonly client: Client,
private readonly config: AppConfig,
) {}
static async connect(config: AppConfig): Promise<TemporalGateway> {
const connection = await Connection.connect({ address: temporalAddress(config) });
return new TemporalGateway(connection, new Client({ connection, namespace: config.temporal.namespace }), config);
}
async execute(command: AppCommand): Promise<unknown> {
const operation: OperationRequest = { operationId: randomUUID(), command };
return await this.client.workflow.execute("operationWorkflow", {
taskQueue: this.config.temporal.taskQueue,
workflowId: `apistate-${command.kind.replaceAll(".", "-")}-${operation.operationId}`,
workflowExecutionTimeout: this.config.temporal.workflowExecutionTimeout,
args: [{
operation,
activityStartToCloseTimeout: this.config.temporal.activityStartToCloseTimeout,
maximumAttempts: this.config.temporal.retry.maximumAttempts,
}],
});
}
async ensureScoreSchedule(): Promise<{ started: boolean; workflowId: string }> {
const workflowId = this.config.temporal.scoreScheduleWorkflowId;
try {
await this.client.workflow.start("scoreRefreshScheduleWorkflow", {
taskQueue: this.config.temporal.taskQueue,
workflowId,
args: [{
intervalMs: this.config.monitor.refreshIntervalMinutes * 60_000,
activityStartToCloseTimeout: this.config.temporal.activityStartToCloseTimeout,
maximumAttempts: this.config.temporal.retry.maximumAttempts,
}],
});
return { started: true, workflowId };
} catch (error) {
if (error instanceof Error && error.name === "WorkflowExecutionAlreadyStartedError") return { started: false, workflowId };
throw error;
}
}
async close(): Promise<void> {
await this.connection.close();
}
}
+67
View File
@@ -0,0 +1,67 @@
import { fileURLToPath } from "node:url";
import { NativeConnection, Worker } from "@temporalio/worker";
import { createServerContext } from "./bootstrap";
import { loadConfig } from "./config";
import { createActivities } from "./dispatcher";
import { requiredOption } from "./runtime-args";
import { temporalAddress, TemporalGateway } from "./temporal-client";
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 context = createServerContext(config, target);
const connection = await NativeConnection.connect({ address: temporalAddress(config) });
const worker = await Worker.create({
connection,
namespace: config.temporal.namespace,
taskQueue: config.temporal.taskQueue,
workflowsPath: fileURLToPath(new URL("./workflows.ts", import.meta.url)),
activities: createActivities({ lottery: context.service, scores: context.monitor }),
});
const temporal = await TemporalGateway.connect(config);
const schedule = await temporal.ensureScoreSchedule();
let state: "ready" | "stopping" = "ready";
const health = Bun.serve({
hostname: target.workerHealthHost,
port: target.workerHealthPort,
fetch: () => Response.json({
ok: state === "ready",
component: "apistate-worker",
state,
namespace: config.temporal.namespace,
taskQueue: config.temporal.taskQueue,
schedule,
}, { status: state === "ready" ? 200 : 503 }),
});
console.log(JSON.stringify({
ok: true,
component: "apistate-worker",
runtime: runtimeId,
health: health.url.toString(),
temporalNamespace: config.temporal.namespace,
temporalTaskQueue: config.temporal.taskQueue,
schedule,
valuesPrinted: false,
}));
let stopping = false;
async function stop(): Promise<void> {
if (stopping) return;
stopping = true;
state = "stopping";
health.stop(true);
worker.shutdown();
}
for (const signal of ["SIGINT", "SIGTERM"] as const) process.on(signal, () => void stop());
try {
await worker.run();
} finally {
await temporal.close();
await connection.close();
context.close();
console.log(JSON.stringify({ ok: true, component: "apistate-worker", state: "stopped", valuesPrinted: false }));
}
+33
View File
@@ -0,0 +1,33 @@
import { continueAsNew, proxyActivities, sleep, workflowInfo } from "@temporalio/workflow";
import type { OperationRequest, ScheduledScoreRefreshInput, WorkflowOptions } from "./contracts";
export interface Activities {
executeOperation(request: OperationRequest): Promise<unknown>;
}
interface OperationWorkflowInput extends WorkflowOptions {
operation: OperationRequest;
}
function activities(options: WorkflowOptions): Activities {
return proxyActivities<Activities>({
startToCloseTimeout: options.activityStartToCloseTimeout,
retry: { maximumAttempts: options.maximumAttempts },
});
}
export async function operationWorkflow(input: OperationWorkflowInput): Promise<unknown> {
return await activities(input).executeOperation(input.operation);
}
export async function scoreRefreshScheduleWorkflow(input: ScheduledScoreRefreshInput): Promise<void> {
const activity = activities(input);
for (let iteration = 0; iteration < 500; iteration += 1) {
await activity.executeOperation({
operationId: `${workflowInfo().runId}:${iteration}`,
command: { kind: "scores.refresh" },
});
await sleep(input.intervalMs);
}
await continueAsNew<typeof scoreRefreshScheduleWorkflow>(input);
}
+1
View File
@@ -4,6 +4,7 @@
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["bun"]
},