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